首页模块
效果
自定义导航栏
参考效果:自定义导航栏的样式需要适配不同的机型。
安全区域 safeArea
定义:从胶囊顶部 - 屏幕底部
作用:不同手机的安全区域不同,适配安全区域能防止页面重要内容被遮挡。
常见业务: 自定义导航栏,适配不同的手机。
解决方案:可通过 uni.getSystemInfoSync()
获取屏幕边界到安全区的距离。
参考代码:在小程序中,设置navigationStyle
为 custom
src/pages/index/index.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>
小结
问: 自定义导航栏,如何适配 ?
- 通过
uni.getWindowInfo()
查出安全区域的top值 - 设置占位符、margin、padding等属性,撑开等同于top值的高度。
NavBar组件封装
静态结构(复制)
1 ) 新建组件
pages/index/components/Navbar.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
<template>
<navbar />
</template>
<script>
import Navbar from "./components/Navbar.vue";
export default {
components: { Navbar },
};
</script>
处理安全区域
步骤:
- 将
safeArea
和胶囊按钮位置信息,存放在vuex
中 - 从vuex中获取
safeArea
- 指定
paddingTop
为safeArea
的top值
代码:
1 ) 将safeArea
和胶囊按钮位置信息,存放在vuex
中
src/store/index.js
const store = new Vuex.Store({
state: {
safeArea: uni.getWindowInfo().safeArea,
capButton: uni.getMenuButtonBoundingClientRect(),
},
... 省略其它代码 ...
});
export default store;
2 ) 从vuex中获取safeArea
<script>
import { mapState } from "vuex";
export default {
computed: {
...mapState(["safeArea"]),
},
};
</script>
3 ) 指定paddingTop
为safeArea
的top值
<template>
<!-- 导航条 -->
<view class="navbar" >
<view class="navbar" :style="{ paddingTop: safeArea.top + 'px' }">
// ... 省略代码
</view>
</template>
轮播图
静态结构(复制)
src\components\Carousel\Carousel.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>
请求数据
步骤:
- 封装API
- 封装请求函数,调用API
- 加载后,触发请求函数
- 声明变量保存数据
步骤:
1 ) 封装API
@/api/home.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
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的目的,是轮播图组件在其它页面也要使用,而且高度不同。
步骤:
- 父传子,
banner
- 子组件,定义
props
- 列表渲染,插值显示
- 处理高度
代码:
src\pages\index\index.vue
<template>
<view>
<navbar />
<!-- 1. 父传子 banners 和 height -->
<Carousel :banners="banners" height="280rpx" />
</view>
</template>
src/components/Carousel/Carousel.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>
// ... 省略样式代码
前台类目
效果:
静态结构(复制)
src\components\CateScroll\CateScroll.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
<template>
<view>
<navbar />
<Carousel :banners="banners" height="280rpx" />
<CateScroll />
</view>
</template>
请求数据、渲染界面
步骤:
- 封装API
- 封装请求函数,调用API
- 加载后,触发请求函数
- 声明变量保存数据
代码:
src/api/home.js
// 1. 封装API
export const getCategoryAPI = () => {
return request({
url: "/home/category/mutli",
});
};
src/pages/home/index.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>
<template>
<view>
<navbar />
<Carousel :banners="banners" height="280rpx" />
<!-- 5. 父传子 -->
<CateScroll :list="categories" />
</view>
</template>
src\components\CateScroll\CateScroll.vue
export default {
// 6. 定义props
props: {
list: Array,
},
};
<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>
动态设置滑动效果
步骤:
- 声明变量left
- 通过transform和 left控制向右位移
- 监听滚动事件,获取向左滚动的百分比
- 修改left值,数据驱动视图
代码:
src/pages/index/index.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>
人气推荐
静态结构(复制)
src/pages/index/index.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>
请求数据、渲染界面
步骤:
- 封装API
- 封装请求函数,调用API
- 加载后,触发请求函数
- 声明变量保存数据
代码:
src/api/home.js
// 1. 封装API
export const getHotsAPI = () => {
return request({
url: "/home/hot/mutli",
});
};
src/index/index.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>
页面滚动
<template>
// 💥💥 修复样式,添加content
<view class="content">
<!-- 1.自定义导航 -->
<Navbar></Navbar>
// 💥💥 修复样式,添加main
<scroll-view scroll-y class="main">
<!-- 2. 轮播图 -->
<!-- 3. 分类栏目 -->
<!-- 4. 人气推荐 -->
<!-- 5. 新鲜好物 -->
</scroll-view>
</view>
</template>
新鲜好物
静态结构(复制)
<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
// 1. 封装API
export const getNewGoodsAPI = (limit = 4) => {
return request({
url: "/home/new",
params: { limit },
});
};
src/pages/index/index.vue
import {
getNewGoodsAPI,
} from "@/api/home";
export default {
onLoad() {
this.loadNewGoods();
},
data() {
return {
/** 新鲜好物 */
newGoods: [],
};
},
methods: {
async loadNewGoods() {
const { result } = await getNewGoodsAPI();
this.newGoods = result;
},
},
};
热门品牌-写死(没有接口)
<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>
猜你喜欢
静态结构
src/index/index.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
<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
// 1. 封装API
export const getLikesAPI = (page = 1, pageSize = 10) => {
return request({
url: "/home/goods/guessLike",
data: { page, pageSize },
});
};
src/pages/index/index.vue
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++,发送请求,新数据追加到旧数据
如无下一页,阻止发送请求。
步骤:
- 声明变量page表示当前页,变量pages表示总页数
- 每次请求成功:page++,保存pages
- 每次请求成功:累加数据,而不是覆盖
- 监听滚动事件onScroll, 如无下一页,阻止发送请求
代码
- 声明变量page表示当前页,变量pages表示总页数
data() {
return {
page: 1,
pages: 1,
};
},
- 每次请求成功:page++,保存pages
- 每次请求成功:累加数据,而不是覆盖
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);
},
- 监听滚动事件
scrolltolower
, 如无下一页,阻止发送请求
<scroll-view
scroll-y
class="main"
@scrolltolower="loadMore"
>
// ... 省略其它标签diamante
</scroll-view>
loadMore() {
if (this.page > this.pages) return;
this.loadLikes();
},
优化-添加节流
目标:避免抖动,同时触发多个分页请求
思路:
- 声明标识loading为true或false,
- 每次分页请求前,将loading改为true,表示正在请求,未结束。
- 每次分页请求后,将loading改为false,表示请,已未结束。
- 滚动事件触发时,如果loading为true,通过return阻止代码执行
代码:
data() {
return {
// 1. 声明标识loading
loading: false,
};
},
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;
},
loadMore() {
// 4. 滚动事件触发时,如果loading为true,通过return阻止代码执行
if (this.loading) return;
if (this.page > this.pages) return;
this.loadLikes();
},
下拉页面
目标:实现下拉刷新
思路:
- 开启可下拉属性
refresher-enabled
- 声明变量
refreshing
,控制refresher-triggered
属性 - 监听下拉刷新事件
- 清空旧数据
- 请求新数据
<scroll-view
scroll-y
class="main"
@scrolltolower
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
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;
});
},