Skip to content
On this page

首页模块

效果

image-20221124223623926

自定义导航栏

参考效果:自定义导航栏的样式需要适配不同的机型。

自定义导航栏

安全区域 safeArea

定义:从胶囊顶部 - 屏幕底部
作用:不同手机的安全区域不同,适配安全区域能防止页面重要内容被遮挡。
常见业务: 自定义导航栏,适配不同的手机。
解决方案:可通过 uni.getSystemInfoSync() 获取屏幕边界到安全区的距离。

安全区域

参考代码:在小程序中,设置navigationStylecustom

src/pages/index/index.vue

vue
<template>
  <view>
    <!-- 3. 使用view充当占位符 --> 
    <!-- 💥💥 核心:同安全区的top坐标值,占位 --> 
    <view :style="{ height: safeArea.top + 'px', backgroundColor: 'red' }" /> 
  </view>
</template>

<script>
export default {
  data() {
    return {
     // 1. 声明变量 
     safeArea: null, 
    };
  },

  onLoad() {
    // 2. 加载后,查询设置的安全区坐标位置, 保存 
    const { safeArea } = uni.getWindowInfo(); 
    this.safeArea = safeArea; 
  },
};
</script>

小结

问: 自定义导航栏,如何适配 ?
  1. 通过uni.getWindowInfo()查出安全区域的top值
  2. 设置占位符、margin、padding等属性,撑开等同于top值的高度。

静态结构(复制)

1 ) 新建组件

pages/index/components/Navbar.vue

vue
<template>
  <!-- 导航条 -->
  <view class="navbar">
    <!-- 文字logo -->
    <view class="logo">
      <image
        src="http://static.botue.com/erabbit/static/images/logo.png"
      ></image>
      <text>新鲜 · 亲民 · 快捷</text>
    </view>
    <!-- 搜索条 -->
    <view class="search">
      <text class="icon-search">搜索商品</text>
      <text class="icon-scan"></text>
    </view>
  </view>
</template>

<script>
export default {
};
</script>

<style lang="scss">
page {
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.navbar {
  background-image: url(http://static.botue.com/erabbit/static/images/navigator_bg.png);
  background-size: cover;
  position: relative;
  overflow: hidden;
  .logo {
    display: flex;
    align-items: center;
    height: 64rpx;
    padding-left: 30rpx;
    image {
      width: 166rpx;
      height: 39rpx;
    }
    text {
      flex: 1;
      line-height: 28rpx;
      color: #fff;
      margin: 2rpx 0 0 20rpx;
      padding-left: 20rpx;
      border-left: 1rpx solid #fff;
      font-size: 26rpx;
    }
  }
  .search {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 10rpx 0 26rpx;
    height: 64rpx;
    margin: 16rpx 20rpx;
    color: #fff;
    font-size: 28rpx;
    border-radius: 32rpx;
    background-color: rgba(255, 255, 255, 0.5);
  }
  .icon-search {
    &::before {
      margin-right: 10rpx;
    }
  }
  .icon-scan {
    font-size: 30rpx;
    padding: 15rpx;
  }
}
</style>

2 ) 使用组件

pages/index/index.vue

vue
<template>
  <navbar /> 
</template>

<script>
import Navbar from "./components/Navbar.vue"; 
export default { 
 components: { Navbar }, 
}; 
</script>

处理安全区域

步骤:

  1. safeArea和胶囊按钮位置信息,存放在vuex
  2. 从vuex中获取safeArea
  3. 指定paddingTopsafeArea的top值

代码:

1 ) 将safeArea和胶囊按钮位置信息,存放在vuex

src/store/index.js

js
const store = new Vuex.Store({
  state: {
    safeArea: uni.getWindowInfo().safeArea, 
    capButton: uni.getMenuButtonBoundingClientRect(), 
  },
  
  ... 省略其它代码 ...
});
export default store;

2 ) 从vuex中获取safeArea

js
<script>
import { mapState } from "vuex"; 
export default {
  computed: {
    ...mapState(["safeArea"]), 
  },
};
</script>

3 ) 指定paddingTopsafeArea的top值

vue
<template>
  <!-- 导航条 -->
  <view class="navbar" > 
  <view class="navbar" :style="{ paddingTop: safeArea.top + 'px' }"> 

  // ... 省略代码
  </view>
</template>

轮播图

image-20221124231800587

静态结构(复制)

src\components\Carousel\Carousel.vue

vue
<template>
  <swiper class="carousel" autoplay indicator-dots circular>
    <swiper-item>
      <image></image>
    </swiper-item>
  </swiper>
</template>

<script>
export default {};
</script>

<style scoped>
.carousel {
  height: 100%;
}
</style>

请求数据

步骤:

  1. 封装API
  2. 封装请求函数,调用API
  3. 加载后,触发请求函数
  4. 声明变量保存数据

步骤:

1 ) 封装API

@/api/home.js

js
import request from "@/utils/http";

export const getBannersAPI = (distributionSite = 1) => {
  return request({
    url: "/home/banner",
    data: {
      distributionSite,
    },
  });
};

2 ) 封装请求函数,调用API

3 ) 加载后,触发请求函数

4 ) 声明变量保存数据

@/pages/index/index.vue

js
import { getBannersAPI } from "@/api/home";
export default {
  // ... 省略其它代码
  
  // 3 ) 加载后,触发请求函数
  onLoad() {
    this.loadBanners();
  },
  data() {
    return {
      banners: [],
    };
  },
  methods: {
    // 2 ) 封装请求函数,调用API
    async loadBanners() {
      const { result } = await getBannersAPI();
      // 4 ) 声明变量保存数据
      this.banners = result;
    },
  },
};

渲染界面

父传子height的目的,是轮播图组件在其它页面也要使用,而且高度不同。

步骤:

  1. 父传子,banner
  2. 子组件,定义props
  3. 列表渲染,插值显示
  4. 处理高度

代码:

src\pages\index\index.vue

vue
<template>
  <view>
    <navbar />
    <!-- 1. 父传子 banners 和 height -->
    <Carousel :banners="banners" height="280rpx" />
  </view>
</template>

src/components/Carousel/Carousel.vue

vue
<template>
  <!-- 4. 设置轮播图高度 -->
  <swiper :style="{height: height}" class="carousel" autoplay indicator-dots circular>
    <!-- 3. 列表渲染, 插值显示 -->
    <swiper-item v-for="item in banners" :key="item.id">
      <image :src="item.imgUrl">
    </swiper-item>
  </swiper>
</template>

<script>
export default {
  // 2. 子组件,定义`props`
  props: {    
    banners: Array,
    height: String,
  },
};
</script>

// ... 省略样式代码

前台类目

效果:

image-20221124232508060

静态结构(复制)

src\components\CateScroll\CateScroll.vue

vue
<template>
  <view class="category-head-mutli">
    <scroll-view scroll-x>
      <view class="scroll-wrap">
        <view class="category-head-mutli-item">
          
          <navigator>
            <image mode="widthFix" />
            <text>文字1</text>
          </navigator>
          
           <navigator>
            <image mode="widthFix" />
            <text>文字2</text>
          </navigator>
          
        </view>
      </view>
    </scroll-view>
    <view class="scroll-bar">
      <view class="scroll-bar-inner"></view>
    </view>
  </view>
</template>

<script>
export default {};
</script>

<style lang="scss">
.category-head-mutli {
  background-color: #f7f7f8;
  position: relative;
  padding-bottom: 30rpx;
  .scroll-wrap {
    width: 200%;
  }
  .category-head-mutli-item {
    width: 50%;
    display: flex;
    flex-wrap: wrap;
    navigator {
      text-align: center;
      width: 20%;
      padding: 15rpx;

      image {
        width: 100rpx;
        display: block;
        margin: 0 auto;
      }
      text {
        font-size: 26rpx;
        color: #666;
      }
    }
  }
  .scroll-bar {
    position: absolute;
    bottom: 15rpx;
    left: 50%;
    transform: translateX(-50%);
    width: 100rpx;
    height: 8rpx;
    background-color: #e2e2e2;
    z-index: 200;
    .scroll-bar-inner {
      width: 50%;
      height: 100%;
      position: absolute;
      left: 0;
      top: 0;
      background-color: #00c09b;
    }
  }
}
</style>

src/index/index.vue

vue
<template>
  <view>
    <navbar />
    <Carousel :banners="banners" height="280rpx" />

    <CateScroll  /> 
  </view>
</template>

请求数据、渲染界面

步骤:

  1. 封装API
  2. 封装请求函数,调用API
  3. 加载后,触发请求函数
  4. 声明变量保存数据

代码:

src/api/home.js

js
// 1. 封装API
export const getCategoryAPI = () => {
  return request({
    url: "/home/category/mutli",
  });
};

src/pages/home/index.vue

vue
<script>
import { getBannersAPI, getCategoryAPI } from "@/api/home";
export default {

  onLoad() {
    this.loadBanners();
    // 3. 加载后,触发请求函数 
    this.loadCategory(); 
  },
  data() {
    return {
      banners: [],
+      // 4. 声明变量保存数据
+      category: [],
    };
  },
  methods: {
    async loadBanners() {
      const { result } = await getBannersAPI();
      this.banners = result;
    },
+    // 2. 封装请求函数,调用API
+    async loadCategory() {
+      const { result } = await getCategoryAPI();
+      this.category = result;
+    },
  },
};
</script>
vue
<template>
  <view>
    <navbar />
    <Carousel :banners="banners" height="280rpx" />
    <!-- 5. 父传子 -->
    <CateScroll :list="categories" /> 
  </view>
</template>

src\components\CateScroll\CateScroll.vue

js
export default {
  // 6. 定义props
  props: {
    list: Array,
  },
};
vue
<template>
  <view class="category-head-mutli">
    <scroll-view scroll-x>
      <view class="scroll-wrap">
        <view class="category-head-mutli-item">
          <!-- 7. 列表渲染 -->  
          <navigator v-for="item in list" :key="item.id"> 
            <image mode="widthFix" :src="item.icon" /> 
            <text>{{ item.name }}</text> 
          </navigator>
        </view>
      </view>
    </scroll-view>
    <view class="scroll-bar">
      <view class="scroll-bar-inner"></view>
    </view>
  </view>
</template>

动态设置滑动效果

scroll-view 组件

步骤:

  1. 声明变量left
  2. 通过transform和 left控制向右位移
  3. 监听滚动事件,获取向左滚动的百分比
  4. 修改left值,数据驱动视图

代码:

src/pages/index/index.vue

vue
<template>
  <view class="category-head-mutli">
    <scroll-view 
    	scroll-x 
    	@scroll="onScroll"  
    >
      <!-- 省略其它代码 -->
      
       <!-- 省略其它代码 -->
    </scroll-view>
    <view class="scroll-bar">
     <!-- 2. 通过style控制向右位移 --> 
      <view
        class="scroll-bar-inner"
        :style="{ transform: `translateX(${left}%)` }" 
      />
    </view>
  </view>
</template>

<script>
import { mapState } from "vuex";
export default {
	// ... 省略其它代码
	
  data() {
    return {
      // 1. 声明变量 
      left: 0, 
    };
  },
  computed: {
    ...mapState(["safeArea"]), 
  },
  methods: {
    onScroll(e) {
     // 3. 监听滚动事件,获取向左滚动的百分比 
     const percent = (e.detail.scrollLeft / this.safeArea.width) * 100; 

     // 4 修改left的值 
     this.left = percent; 
    },
  },
};
</script>

人气推荐

image-20221124233715508

静态结构(复制)

src/pages/index/index.vue

vue
<template>
  
  <view class="content">
    <navbar />
    <Carousel :banners="banners" height="280rpx" />
    <CateScroll :list="categories" />

    <!--  热门推荐 -->
    <view class="panel recommend">
      <view class="item" v-for="item in hots" :key="item.id">
        <view class="title">
          {{ item.title }}<text>{{ item.alt }}</text>
        </view>
        <navigator
          hover-class="none"
          :url="`/pages/recommend/index?type=${item.type}`"
          class="cards"
        >
          <image
            mode="aspectFit"
            v-for="img in item.pictures"
            :key="img"
            :src="img"
          ></image>
        </navigator>
      </view>
    </view>
  </view>
</template>

请求数据、渲染界面

步骤:

  1. 封装API
  2. 封装请求函数,调用API
  3. 加载后,触发请求函数
  4. 声明变量保存数据

代码:

src/api/home.js

js
// 1. 封装API
export const getHotsAPI = () => {
  return request({
    url: "/home/hot/mutli",
  });
};

src/index/index.vue

vue
<script>

import { getBannersAPI, getCategoryAPI, getHotsAPI } from "@/api/home";
export default {
  onLoad() {
    
		// 3. 加载后,触发请求函数 
    this.loadHots(); 
  },
  data() {
    return {

      /** 4. 声明变量,保存数据-热门推荐 */ 
      hots: [], 
    };
  },
  methods: {
   // 2. 封装请求函数,调用API 
   async loadHots() { 
      const { result } = await getHotsAPI(); 
      this.hots = result; 
    },
  },
};
</script>

页面滚动

vue
<template>
// 💥💥 修复样式,添加content
  <view class="content">
    <!-- 1.自定义导航 -->
    <Navbar></Navbar>
    // 💥💥 修复样式,添加main
    <scroll-view scroll-y class="main">
      <!-- 2. 轮播图 -->
      <!-- 3. 分类栏目 -->
      <!-- 4. 人气推荐 -->
      <!-- 5. 新鲜好物 -->
    </scroll-view>
  </view>
</template>

新鲜好物

image-20221124233742310

静态结构(复制)

vue
<view class="panel fresh">
    <view class="title">
      新鲜好物
      <navigator
        hover-class="none"
        class="more"
        url="/pages/recommend/index?type=5"
        >更多</navigator
      >
    </view>
    <view class="cards">
      <navigator
        hover-class="none"
        :url="`/pages/goods/index?id=${item.id}`"
        v-for="item in newGoods"
        :key="item.id"
      >
        <image mode="aspectFit" :src="item.picture"></image>
        <view class="name">{{ item.name }}</view>
        <view class="price">
          <text class="small">¥</text>{{ item.price }}
        </view>
      </navigator>
    </view>
  </view>

请求数据、渲染界面

src/api/home.js

js
// 1. 封装API
export const getNewGoodsAPI = (limit = 4) => {
  return request({
    url: "/home/new",
    params: { limit },
  });
};

src/pages/index/index.vue

vue
import {
  getNewGoodsAPI, 
} from "@/api/home";
export default {

  onLoad() {

    this.loadNewGoods(); 
  },
  data() {
    return {

      /** 新鲜好物 */
      newGoods: [], 
    };
  },
  methods: {

    async loadNewGoods() { 
      const { result } = await getNewGoodsAPI(); 
      this.newGoods = result; 
    }, 
  },
};

热门品牌-写死(没有接口)

image-20221124233832944

vue
<view class="panel brands">
        <view class="title">
          热门品牌
          <navigator hover-class="none" class="more" url="/pages/list/index"
            >更多</navigator
          >
        </view>
        <view class="cards">
          <navigator hover-class="none" url="/pages/goods/index">
            <image
              mode="aspectFit"
              src="http://static.botue.com/erabbit/static/uploads/brand_logo_1.jpg"
            ></image>
            <view class="name">小米</view>
            <view class="price">99元起</view>
          </navigator>
          <navigator hover-class="none" url="/pages/goods/index">
            <image
              mode="aspectFit"
              src="http://static.botue.com/erabbit/static/uploads/brand_logo_2.jpg"
            ></image>
            <view class="name">TCL</view>
            <view class="price">199起</view>
          </navigator>
          <navigator hover-class="none" url="/pages/goods/index">
            <image
              mode="aspectFit"
              src="http://static.botue.com/erabbit/static/uploads/brand_logo_3.jpg"
            ></image>
            <view class="name">饭小宝</view>
            <view class="price">9.9起</view>
          </navigator>
          <navigator hover-class="none" url="/pages/goods/index">
            <image
              mode="aspectFit"
              src="http://static.botue.com/erabbit/static/uploads/brand_logo_4.jpg"
            ></image>
            <view class="name">鳄鱼</view>
            <view class="price">299起</view>
          </navigator>
        </view>
      </view>

猜你喜欢

image-20221124233907227

静态结构

src/index/index.vue

vue
<template>
  <view class="content">
    <!-- 1.自定义导航 -->
    <Navbar></Navbar>
    <scroll-view scroll-y class="main">
      <!-- 2. 轮播图 -->
      <!-- 3. 分类栏目 -->
      <!-- 4. 人气推荐 -->
      <!-- 5. 新鲜好物 -->
      <!-- 6. 热门品牌 -->
      
     <!-- 7. 猜你喜欢 --> 
     <Guess :list="likes" /> 
    </scroll-view>
  </view>
</template>

src\components\Guess\Guess.vue

vue
<template>
  <view class="guess-wrap">
    <view class="caption">
      <text class="text">猜你喜欢</text>
    </view>
    <view class="guess">
      <navigator
        class="navigator"
        v-for="item in list"
        :key="item.id"
        :url="`/pages/goods/index?id=${item.id}`"
      >
        <image class="image" mode="aspectFill" :src="item.picture"></image>
        <view class="name">{{ item.name }}</view>
        <view class="price">
          <text class="small">¥</text>
          {{ item.price }}
          <text class="small">.00</text>
        </view>
      </navigator>
    </view>
    <!-- 没有更多数据了 -->
    <view class="loading" v-if="false">正在加载...</view>
  </view>
</template>

<script>
export default {
  props: {list: Array}
};
</script>

<style lang="scss">
/* 分类标题 */

.caption {
  display: flex;
  justify-content: center;
  line-height: 1;
  padding: 36rpx 0 40rpx;
  font-size: 32rpx;
  color: #262626;
  .text {
    display: block;
    padding: 0 28rpx 0 30rpx;
    position: relative;
    &::before,
    &::after {
      content: "";
      position: absolute;
      top: 6rpx;
      width: 20rpx;
      height: 20rpx;
      background-image: url(http://static.botue.com/erabbit/static/images/bubble.png);
      background-size: contain;
    }
    &::before {
      left: 0;
    }
    &::after {
      right: 0;
    }
  }
}

/* 猜你喜欢 */
.guess {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  padding: 0 20rpx;
  .navigator {
    width: 345rpx;
    padding: 24rpx 20rpx 20rpx;
    margin-bottom: 20rpx;
    border-radius: 10rpx;
    overflow: hidden;
    background-color: #fff;
  }
  .image {
    height: 260rpx;
  }
  .name {
    height: 75rpx;
    margin: 10rpx 0;
    font-size: 26rpx;
    color: #262626;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
  .price {
    line-height: 1;
    padding-top: 4rpx;
    color: #cf4444;
    font-size: 26rpx;
  }
  .small {
    font-size: 80%;
  }
}
</style>

请求数据、渲染界面

src/api/home.js

js
// 1. 封装API
export const getLikesAPI = (page = 1, pageSize = 10) => {
  return request({
    url: "/home/goods/guessLike",
    data: { page, pageSize },
  });
};

src/pages/index/index.vue

js
import {   getLikesAPI } from "@/api/home"; 
export default {

  onLoad() {

    this.loadLikes(); 
  },
  data() {
    return {

      /** 猜你喜欢 */
      likes: [], 
    };
  },
  methods: {

		async loadLikes() { 
      const { result } = await getLikesAPI(); 
      const { items } = result; 
      this.likes = items; 
   },
  },
};

上拉分页

目标:当页面滚动到底部时,触发 滚动条触底事件,执行分页业务

思路:

本质是分页请求,保存当前页数page,保存总页数pages。

判断还有没有下一页: 当前页数page < 后台告知的总页数pages

如有下一页,page++,发送请求,新数据追加到旧数据

如无下一页,阻止发送请求。


步骤:

  1. 声明变量page表示当前页,变量pages表示总页数
  2. 每次请求成功:page++,保存pages
  3. 每次请求成功:累加数据,而不是覆盖
  4. 监听滚动事件onScroll, 如无下一页,阻止发送请求

代码

  1. 声明变量page表示当前页,变量pages表示总页数
vue
data() {
  return { 
    page: 1,  
    pages: 1, 
  };
},
  1. 每次请求成功:page++,保存pages
  2. 每次请求成功:累加数据,而不是覆盖
js
async loadLikes() {
   const { result } = await getLikesAPI(this.page);
   const { items } = result; 
   this.likes = items; 

  const { items, pages } = result; 
  this.page++; 
  this.pages = pages; 

  this.likes.push(...items); 
 },
  1. 监听滚动事件scrolltolower, 如无下一页,阻止发送请求
vue
<scroll-view 
	scroll-y 
	class="main" 
	@scrolltolower="loadMore" 
>
  // ... 省略其它标签diamante
</scroll-view>
js
loadMore() {
   if (this.page > this.pages) return;
   this.loadLikes();
 },

优化-添加节流

目标:避免抖动,同时触发多个分页请求

思路:

  1. 声明标识loading为true或false,
  2. 每次分页请求前,将loading改为true,表示正在请求,未结束。
  3. 每次分页请求后,将loading改为false,表示请,已未结束。
  4. 滚动事件触发时,如果loading为true,通过return阻止代码执行

代码:

vue
data() {
  return {
    // 1. 声明标识loading 
    loading: false, 
  };
},
vue
async loadLikes() {
  // 2. 每次分页请求前,将loading改为true,表示正在请求,未结束。
  this.loading = true; 

  const { result } = await getLikesAPI(this.page);
  const { items, pages } = result;
  this.page++;
  this.pages = pages;
  this.likes.push(...items);

  // 3. 每次分页请求后,将loading改为false,表示请,已未结束。 
  this.loading = false; 
 },
vue
loadMore() {
  // 4. 滚动事件触发时,如果loading为true,通过return阻止代码执行 
  if (this.loading) return; 

   if (this.page > this.pages) return;
   this.loadLikes();
 },

下拉页面

目标:实现下拉刷新

思路:

  1. 开启可下拉属性refresher-enabled
  2. 声明变量refreshing,控制refresher-triggered属性
  3. 监听下拉刷新事件
  4. 清空旧数据
  5. 请求新数据
vue
<scroll-view      
      scroll-y
      class="main"
      @scrolltolower
      refresher-enabled 
      :refresher-triggered="refreshing" 
      @refresherrefresh="onRefresh" 
    >
js
async onRefresh() {
  // 清空旧数据
  this.refreshing = true;
  this.banners = [];
  this.categories = [];
  this.hots = [];
  this.newGoods = [];
  this.likes = [];
  this.loading = false;
  this.page = 1;
  this.pages = 1;

  // 请求新数据
  Promise.all([
    this.loadBanners(),
    this.loadCategory(),
    this.loadHots(),
    this.loadNewGoods(),
    this.loadLikes(),
  ]).then(() => {
    this.refreshing = false;
  });
},

Released under the MIT License.