商城小程序

675 阅读34分钟

这个小程序的笔记是 2022/7/2 写完的,有些地方我没有写到的应该是我忘了。因为我不是当时就写了,而且后面凭借自己记忆写的。如果有什么不对?你可以提出来,我们交流交流

整一个技术是用 uniapp开发的 ,因为这个是vue的写法我更加属性,一开始用编译器是 HBuilderX 后面搬到了 vs code的 我自己是有一点碎碎念的。

项目开始配置

搭建项目的页面

HBuilderX 修改了小程序里面也会进行修改的,所以要在 HBuilderX 里面修改才可以

0.1.png

0.png点击这个才会有生成文件夹已经配置路由

1.png 先运行小程序看是否能运行起来

1.2.png

1.3.png

右边这样就编译成功了,就会生成左边unpackage文件夹里面dev后面mp-weixin复制路径去小程序粘贴就可以看看是否能运行

小程序的页面要配置路由:

这个路由是在pages.json里面pages数组中设置的,这个编译器自动帮我们设置好了

 {
            这里配置可以搜索微信文档:全局配置:路由地址
            "path" : "pages/category/category",
            "style" :                                                                                    
            {
            这里配置可以搜索微信文档:页面配置:设置这个页面的标题
                "navigationBarTitleText": "",
                "enablePullDownRefresh": false
            }
   }

配置顶部导航

顶部导航是在pages.json里面globalStyle对象里面配置的,可以看微信文档里面的全局配置

"globalStyle": {
    顶部导航标题颜色
		"navigationBarTextStyle": "white",
    顶部导航名字
		"navigationBarTitleText": "黑马优购",
    顶部导航背景颜色
		"navigationBarBackgroundColor": "#eb4450",
    下拉的背景颜色
		"backgroundColor": "#F8F8F8"
	}

配置底部导航

配置底部icon图标和文字,可以看微信文档里面的全局配置

存放图片位置是 static 文件夹里面

"tabBar": {
		// tab选择中时候文字样式
		"selectedColor": "#e03440",
		// list起码配置两个,最多是五个
		"list": [{
				// 默认图标路径
				"iconPath": "static/icons/home.png",
				// 选中图标路径
				"selectedIconPath": "static/icons/home-o.png",
				//点击选择的网页路径
				"pagePath": "pages/index/index",
				// 底部导航标题
				"text": "首页"
			},
			{
				// 默认图标路径
				"iconPath": "static/icons/category.png",
				// 选中图标路径
				"selectedIconPath": "static/icons/category-o.png",
				"pagePath": "pages/category/category",
				"text": "购物车"
			}]
		}

下载插件

1.4.png

1.7.png 1、下载 uView2.0重磅发布,利剑出鞘,一统江湖 这个版本的

2、www.uviewui.com/components/… 官网配置

1.8.png

配置步骤

里面要@/开头才是正确的

HBuilderX 会给 main.js 配套vue2、vue3.我们选择vue2就可以删除vue3

1.9.png

#1. 引入uView主JS库

在项目根目录中的main.js中,引入并使用uView的JS库,注意这两行要放在import Vue之后。

// main.js
import uView from '@/uni_modules/uview-ui'
Vue.use(uView)

2. 在引入uView的全局SCSS主题文件

在项目根目录的uni.scss中引入此文件。

/* uni.scss */
@import '@/uni_modules/uview-ui/theme.scss';

3. 引入uView基础样式

注意!

App.vue首行的位置引入,注意给style标签加入lang="scss"属性

<style lang="scss">
	/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
	@import "@/uni_modules/uview-ui/index.scss";
</style>

4. 配置easycom组件模式

此配置需要在项目根目录的pages.json中进行。

// pages.json
{
	// 如果您是通过uni_modules形式引入uView,可以忽略此配置
	"easycom": {
		"^u-(.*)": "@/uni_modules/uview-ui/components/u-$1/u-$1.vue"
	},
	
	// 此为本身已有的内容
	"pages": [
		// ..
	]
}

设置uView button按钮

首页

效果

2.0.1.png

搜索框

搜索框的全局封装,在vue中我们习惯把全局组件放在components里面的。这里 HBuilderX 编辑器就跟着我们这个vue习惯进行了修改。只要我们根目录下面有 components 这个文件夹

2.0.png 就会出现 新建组件这个文件夹自动创建 全局的组件 ,不需要我们注册。如果我们注册了就会报错了

2.1.png

2.2.png 这样就可以创建完成 然后引入 文档里面的 Search 搜索 完成了这个组件的封装

<template>
	<view class="MySearch">
	shape	搜索框形状,round-圆形,square-方形
	showAction	是否显示右侧控件(右侧的"搜索"按钮)
		<u-search placeholder="日照香炉生紫烟" v-model="keyword" shape='square' :showAction="false"></u-search>
	</view>
</template>

<script>
	export default {
		name: "MySearch",
		data() {
			return {
				keyword: ''
			};
		}
	}
</script>

<style lang="scss" scoped>
	.MySearch {
		padding: 10rpx 24rpx;
		background-color: #ea4350;
	}
</style>

在index里面直接可以引入这个组件(MySearch)了,又因为微信小程序用 rpx 这个单位。只要750px是1:1的等比例转换。rpx就可以在任何屏幕适配了,如果设计稿不是750px就不能使用rpx

2.3.png

      <view>
		<!-- 编辑器自动帮我们注册了全局组件 -->
		<MySearch></MySearch>
	</view>

轮播图

这里搜索框使用微信里面的 swiper 就足够了,我们想给出来图片适配在搞其他的

用一个盒子装起来
<view class="banner">

<swiper indicator-dots indicator-active-color="#fff" autoplay circular
				indicator-color="rgba(255,255,255,0.5)">
				<swiper-item>
					<image src="https://api-hmugo-web.itheima.net/pyg/banner1.png">                         </image>
				</swiper-item>
</view>


css 这些都是看着效果图的
<style lang="scss" scoped>
	.banner {
		width: 750rpx;
		height: 340rpx;

		image {
			width: 100%;
			height: 100%;
		}
	}
</style>

请求

axios只能在pc还有node上面使用,小程序是用不了的。但是小程序(uView) 有封装好的给我们用

2.4.png 在发起请求之前我们有在 微信公众号 网页配置访问地址 不然会出现跨域的错误

2.5.png

2.6.png 在微信小程序平台上面,出现选择的域名就是成功了

2.7.png

async onLoad() {
			// 🎯 前置工作:微信公众平台 -> 添加 request 合法域名
			//      https://api-hmugo-web.itheima.net
			// 获取轮播图数据
			// 为什么返回数据是一个数组,因为这个组件请求是Promise封装的,所以如果有错误数组第一个就是错误。第二个就是空,反之亦然
			const [error, res] = await uni.request({
				method: 'GET',
				url: 'https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata'
			})
			if (error) {
				console.log(error);
			} else {
				console.log('成功', res);
			}
		},

获取数据成功之后,根据数据进行v-for渲染

<!-- 轮播图模块 -->
		<view class="banner">
		
			indicator-dots:是否显示面板指示点
			indicator-color:指示点颜色
			indicator-active-color:当前选中的指示点颜色	
			autoplay:是否自动切换	
			circular:是否采用衔接滑动
			
			<swiper indicator-dots indicator-active-color="#fff" autoplay circular
				indicator-color="rgba(255,255,255,0.5)">
				<swiper-item v-for="v in list" :key="v.goods_id">
					<image :src="v.image_src"></image>
				</swiper-item>
			</swiper>
		</view>

因为首页一般都是要下拉刷新的,需要把这个反正成为函数方便到时候调用接口函数刷新数据,后面的都会这样做

async getBanner() {
				// 🎯 前置工作:微信公众平台 -> 添加 request 合法域名
				//      https://api-hmugo-web.itheima.net
				// 获取轮播图数据
				// 为什么返回数据是一个数组,因为这个组件请求是Promise封装的,所以如果有错误数组第一个就是错误。第二个就是空,反之亦然
				const [error, res] = await uni.request({
					method: 'GET',
					url: 'https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata'
				})
				if (error) {
					console.log(error);
				} else {
					this.list = res.data.message
				}
			},

导航

封装函数发起请求

async getNav() {
				const [error, res] = await uni.request({
					method: "GET",
					url: 'https://api-hmugo-web.itheima.net/api/public/v1/home/catitems'
				})
				if (error) {
					console.log(error);
				} else {
					this.navList = res.data.message
				}
},

2.6.5.png 这里需要点击跳转所以,要使用 navigator 第一个分类是跳转底部导航的分类页面

	<!-- 导航模块 -->
		<view class="nav">
			<!-- 
			open-type----跳转方式
			 
			 -->
			<navigator v-for="v in navList" :key="v.name" :open-type="v.navigator_url||''" url="v.navigator_url ||''">
				<image :src="v.image_src" mode="aspectFill"></image>
			</navigator>
		</view>

楼层

封装函数发起请求

async getFloor() {
				const [error, res] = await uni.request({
					method: "GET",
					url: 'https://api-hmugo-web.itheima.net/api/public/v1/home/catitems'
				})
				if (error) {
					console.log(error);
				} else {
					this.floorList = res.data.message
					console.log(this.floorList);
				}
			}

2.8.png 得到这样的数组对象,里面第一个对象是楼层的标题,里面数组是下面的图片。而且里面是有导航的属性的,需要有一个导航标签套起来里面有图片 ,里面固定了宽度,图片有一个属性是 widthFix 根据宽度定高度。但是有一些手机是不适配的。

结构是 一个大盒子(v-for) 里面,里面一个标题 。里面是内容 导航里面是图片,在导航里面进行二次遍历

<!-- 楼层模块 -->
		    <view class="floor" v-for="value in floorList" :key="value.name">
      <view class="floor-title"
        ><image :src="value.floor_title.image_src" mode="scaleToFill"
      /></view>
      <view class="floor-content">
        <navigator
          v-for="item in value.product_list"
          :key="item.name"
          :url="item.navigator_url"
          :open-type="item.open_type"
        >
          <image
            :src="item.image_src"
            :style="{ width: item.image_width + 'rpx' }"
          />
        </navigator>
      </view>
    </view>

2.9.png

1656290079833.png 就需要顶部图片需要给宽度还有高度,下面的图片使用浮动就可以。实现并排在一起,浮动一开始就是用在图文环绕的效果的。而且这里有一个大图片,

不能使用flex不然那个大图片也浮动了。要flex就要四个图片在一个盒子里面。通过v-for很难实现,所以使用左浮动

.floor {

		.floor-title {
			image {
				width: 750rpx;
				height: 59rpx;
			}
		}

		.floor-content {

			image {
				// 浮动可以实现环绕效果
				float: left;
				// 最小高, 图片大于等于 188rpx
				min-height: 188rpx;
				// 间距
				margin-left: 8rpx;
				margin-bottom: 8rpx;
			}

		}

下拉刷新

1、我们要实现下拉刷新,起码是要下拉这个功能实现吧。

我们全局配置下拉都是false只有特殊页面才需要下拉的

"path": "pages/index/index",
			"style": {
				// 下拉刷新是页面的,全局的是false。前面要下拉才能监听这个事件
				这个是页面配置来的
				"enablePullDownRefresh": true,
				"navigationBarTitleText": "黑马优购首页"
}

2、 小程序给我们配置了下拉刷新的生命周期函数 onPullDownRefresh

  • 可以通过 uni.startPullDownRefresh 或者是 enablePullDownRefresh 触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。
  • 当处理完数据刷新后,uni.stopPullDownRefresh 可以停止当前页面的下拉刷新。

我们调用onPullDownRefresh,因为我们之前三个请求(轮播图,导航,楼层都是通过请求回来的)我们都进行了封装,所以我们这里直接调用这三个函数就可以了

onPullDownRefresh() {
this.getBanner(), this.getNav(), this.getFloor()
  },

优化操作

1、 // 在我们移动端头条项目里面下拉刷新获取数据和之前数据一样,就会报错id重复的错误

 onPullDownRefresh() {
 在调用之前清空一下之前数组就可以了
     this.list = [];
    this.navList = [];
    this.floorList = [];
this.getBanner(), this.getNav(), this.getFloor()
  },

2、 我们在手机上测试发现,下拉刷新不会恢复原来的样子。

所以我们需要三个接口函数回来之后在调用uni.stopPullDownRefresh 可以停止当前页面的下拉刷新。

这里我们用到了// Promise有一个all方法那个输入的所有 promise 的 resolve 回调的结果是一个数组。以及给用户提示信息了。

 async onPullDownRefresh() {
    // 在我们移动端头条项目里面下拉刷新获取数据和之前数据一样,就会报错id重复的错误
    this.list = [];
    this.navList = [];
    this.floorList = [];
    // Promise有一个all方法那个输入的所有 promise 的 resolve 回调的结果是一个数组。
    await Promise.all([this.getBanner(), this.getNav(), this.getFloor()]);
    // 所有请求完成后,隐藏下拉刷新
    uni.stopPullDownRefresh();
    uni.showToast({
      title: "刷新成功",
      duration: 2000,
    });
  },

分类页面

效果图

1656401045513.png

结构:从上往下看。

1、有一个搜索框,我们已经封装过了。

2、这里是大盒子包裹两个盒子,分别是左右结构,左边是分类的标题,右边是分类的内容。

我们先引入之前封装的组件 ,在获取数据打印看看。

因为是第二次在接口封装过了。
// 商品分类
export function classify() {
    return request({
        url: '/categories'
    })
}

<script>
import { classify } from "@/api/home";
export default {
  onLoad() {
    this.getCateList();
  },
  methods: {
    async getCateList() {
      const arr = await classify();
      console.log(arr);
    },
  },
};
</script>

1656401619081.png

 async getCateList() {
      const arr = await classify();
      this.cateList = arr.message;
      //   console.log(this.cateList);
      // 得到数据太多了,我们可以把后台返回数据提取出来我们要的。name(分类的标题)还有ID(渲染的唯一标识),新的数组和后台返回的数据进行一个映射
      this.leftList = this.cateList.map((v) => {
        return {
          cat_name: v.cat_name,
          cat_id: v.cat_id,
        };
      });
     },
  },
 // 我们要是两个数据,这样写太麻烦了。有没有更简单的写法?解构,const obj = {name:'ww'}  我们要定义一个变量获取里面的name值怎么办?
      // 用解构就可以了 const {name} = obj  这样name变量得到的是 'ww' 了  如果我们想让解构  是我们自己定义的变量怎么办? 在 那个属性名后面冒号: 后面接着变量名就好了 const {name:eee} = obj
      //   const {name:[qq]} = obj  就会把  'ww'当成数组返回一个给我们 ('w')
      //   多层 解构  就是属性名 冒号: 后面接着 {要解构的属性名就好了}
      // 像map这样的 一般用箭头函数,左边小括号里面的是  数组里面的每一个对象
      // 右边是表达式 ,左边对象一般是value但是我们只要里面的   cat_name, cat_id  加一个  大括号{ cat_name, cat_id }就表示  箭头函数参数两个以及两个以上就要加括号了
      // 获取里面的这两个属性  右边是表达式 为什么加一个()呢?
      // 因为右边表达式没有加括号就是 函数体的括号了,在箭头函数里面  括号里面需要返回值。这样就不能代表 解构 的 表达式了
      //   所以用一个括号 () 就可以表示  解构 的表达式了
      this.leftList = this.cateList.map(({ cat_name, cat_id }) => ({
        cat_name,
        cat_id,
      }));
      console.log(this.leftList);
    },
        
        <template>
  <view>
    <MySearch></MySearch>
    <view class="category">
      <view class="category-left">
        <view v-for="v in leftList" :key="v.cat_id">{{ v.cat_name }}</view>
      </view>
      <view class="category-right">右边侧边栏</view>
    </view>
  </view>
</template>

<style lang='scss'>
.category {
  display: flex;
  .category-left {
    width: 182rpx;
    background-color: pink;
  }
  .category-right {
    flex: 1;
    background-color: aqua;
  }
}
</style>

上面两种方法是一样的,没有本质的区别。

遍历出来之后进行,给大盒子一个 display: flex; display: flex; 设置左边的宽度 ,剩下的给右边一个flex:1;就可以解决了。

1656405181194.png

这里有一个小细节:

我一滑动,左右两边一起滑动了。

我用过商品小程序时候,我右边内容滑动和我左边的分类标题是没有关系的。(就是我右边滑到最下面,左边的标题依然是没有改变位置的),也就是不能用view进行大盒子的布局。因为view作为大盒子就会一起滑动,你右边滑动到最下面左边也跟着滑动了。导致用户体验不好。

小程序有一个盒子就是:scroll-view 可滚动视图区域。使用竖向滚动时,需要给scroll-view一个固定高度,通过 WXSS 设置 height。组件属性的长度单位默认为px,2.4.0起支持传入单位(rpx/px)。

上面的是微信的文档:重点来了

1、使用竖向滚动时 (这里就要设置, scroll-y 因为 横向滚动 和 纵向滚动 都不会开启的 )

2、要给scroll-view一个固定高度,通过 WXSS 设置 height。也就是说 要 横向滚动 就要设置宽度 ,纵向滚动 就有设置高度了。

我们这里是 纵向滚动 就有设置 高度了,但是这里高度怎么计算呢?

不能给固定的吧,数据都没有确定?怎么可以给固定高度呢?

在CSS里面有一个 calc 计算的 ,我们只要 全部高度 - 顶部搜索框高 根据设计稿就是 顶部搜索框高80 上下距离10 一共100

calc重点:

height: calc(100vh-100rpx); - 这里如果没有空格就会,认为是一起的。css识别不了就会报错。

1656408061594.png

1656405181194.png

1656408099063.png 左边就是没有加 scroll-view 用传统的view布局的,而右边是用 scroll-view 的 左边滚动和右边没有任何关系到。

这里我们先渲染右边的列表,先看效果图。

1656408394568.png 右边一个大盒子( scroll-view ),里面一个标题。下面是大盒子里面进行 v-for 的渲染 ,这里要用导航标签。

因为我们购物,切换分类点击之后跳转到是分类列表页面展示更多的分类页面给我们。点击分类页面进去就是商品详情页面了,这就是商品类型的功能逻辑。

又因为后台返回数据是所有的商品列表,我们要一个标识在数组里面找到对应的商品,提取出来渲染。

我们可以通过点击事件来做,改变对应的标识来做。vue里面一个数据是根据另一个数据计算得出来的结果。

  <!-- 这里渲染是二级标题  里面的children -->
        <view v-for="item in rigthList" :key="item.cat_id">
          <view>{{ item.cat_name }}</view>
          <view>
            <!-- children里面有要渲染的图片还有文字 -->
            <view v-for="value in item.children" :key="value.cat_id">
              <image :src="value.cat_icon" mode="scaleToFill" />
              <view>{{ value.cat_name }}</view>
            </view>
          </view>
        </view>
        
         rigthList() {
      // 数据没有回来时候,this.cateList是一个空数组里面是 undefined就会报错
      const arr =
        this.cateList.length > 0 ? this.cateList[this.index].children : [];
      console.log(arr);
      return arr;
    },

1656411358985.png 看着效果图,发现如果按照效果图给 大盒子 padding 和图片文字盒子 margin 隔开。

字在四五个就会让一行放不下三个的,换一个想法我们一行是100 / 3 就可以得到33.3333% 小数点后面越多越好。这样更加精确,一行就会有这么多宽度绝对放得下的。

<style lang='scss'>
.category {
  display: flex;
  .category-left {
    width: 182rpx;
    height: calc(100vh - 100rpx);
    background-color: #f3f3f3;
    view {
      height: 70rpx;
      text-align: center;
      line-height: 70rpx;
    }
    .active {
      background-color: #fff;
      color: #ea4e5a;
      position: relative;
      &::after {
        content: "";
        width: 4rpx;
        height: 43rpx;
        background-color: #ea4e5a;
        position: absolute;
        left: 0;
        top: 16rpx;
      }
    }
  }
  .category-right {
    flex: 1;
    height: calc(100vh - 100rpx);
    image {
      width: 120rpx;
      height: 120rpx;
    }
    .group {
      text-align: center;
      .group-title {
        font-size: 26rpx;
        margin: 20rpx 0;
      }
      .group-list {
        display: flex;
        flex-wrap: wrap;
        padding: 0 48rpx;

        view {
          margin: 15rpx 0;
          width: 33.33333%;
          font-size: 21rpx;

          display: flex;
          flex-direction: column;
          align-items: center;
        }
      }
    }
  }
}
</style>

1656466436288.png

这个字体颜色已经那个红色的小框框,都是通过一个样式确定的。这个样式上面写了,小框框是固定定位上去的。

通过变量,这个变量默认是 0 也就是匹配索引。在绑定一个点击事件就可以实现切换右边的内容

因为右边内容是通过计算属性改变的,我们在里面同时绑定了这个变量。当这个变量改变之后右边内容也会找到数组对应对象的进行修改

<view
          :class="{ active: index === i }"
          v-for="(v, i) in leftList"
          :key="v.cat_id"
          @click="changeIndex(i)"
          >{{ v.cat_name }}</view
        >
   
    changeIndex(i) {
      this.index = i;
    },

商品列表页面

1656206889938.png 我们先注册一个商品列表页面( goods_list )

1656206790278.png 加上下面的命令就可以

goods_list/goods_list.vue 配置同名的文件夹以及一个同名的vue文件

参数名必选类型说明
querystring关键字
cidstring分类id
pagenumnumber页码
pagesizenumber页容量
$router.push('/employees/detail/' + scope.row.id) 在 vue 路由里面可以通过 query(就是路径后面拼接的)本质上和小程序传参是一样的 问号后面传参数

1656916363416.png

1656209723181.png 我们就要在商品列表页面进行接收,onLoad 生命周期函数 @param query打开当前页面路径中的参数

都是拼接到路径后面的,这里是中文转码了。

1656210582133.png

data() {
    return {
      goodData: {
      拼接到这里方便发起请求
        query: "",
        cid: "",
        pagenum: 1,
        pagesize: 10,
      },
    };
  },
onLoad(query) {
  这里传过来的参数进行拼接
    this.goodData.query = query.query;
    this.goodData.cid = query.cid;
    this.getGoodsList();
  },
  async getGoodsList() {
      // const [error, res] = await uni.request({
      //   url: "/goods/search",
      //   method: "GET",
      data	Object/String/ArrayBuffer	否		请求的参数  uniapp请求参数都是  data的
      axios里面  get / del 是用params 请求的参数  post才是 data 
      //   data: this.goodData,
      // });
      // if (res) {
      //   console.log(res);
      // }
}

优化:

1、发现这样就太麻烦了,能不能像一起一样请求拦截器,以及响应拦截器呢?

之前vue项目都是放在 utils 文件里面定义 request.js 配置拦截器的

// 定义基本路径
const url = 'https://api-hmugo-web.itheima.net/api/public/v1'
// 这里  request 是一个箭头函数 的  名字
// config是这个函数的形参 是一个对象
const request = async config => {
    // 这里是进行短路运算符  如果有method就用传过来的没有就默认是  GET
    config.method = config.method || 'GET'
    // 基本路径加传过来的路径,拼接成为完整的路径
    config.url = url + config.url
    // 在这里发起请求,小程序的请求都是经过Promise对象封装过的
    const [error, res] = await uni.request(config)
    // 如果有错误直接返回错误,阻止下面执行
    if (error) return console.log('错误', error);
    // 没有错误返回请求回来参数
    return res
}
// 导出这个函数
export default request

1656210133251.png 2、 封装请求API函数统一管理

之前vue项目都是在api里面定义函数,我们直接调用这个就可以了。

import request from "@/utils/request";
// 商品列表搜索
export function getGoodsSearchList(data) {
    return request({
        url: "/goods/search",
        data
    });
}
import { getGoodsSearchList } from "@/api/goods";

async getGoodsList() {
      const res = await getGoodsSearchList(this.goodData);
}

结构准备

1、输入框我们之前封装过了,所以直接引入就好了

2、使用UView里面的 Tabs 标签

1656212140199.png lineColor 滑块颜色

lineWidth 滑块长度

scrollable 菜单是否可滚动 移动端一般导航都会比较多要滚动,所以不会撑满。默认true,也就是下面这种情况

1656224625590.png

 <u-tabs
      :scrollable="false"
      :list="tabsList"
      lineWidth="50"
      @click="click"
      lineColor="#eb4450"
    ></u-tabs>
<script>
	export default {
		data() {
			return {
                tabsList: [{ name: "综合" }, { name: "销量" }, { name: "价格" }],
 }
        }
    }
</script>

看了看文档发现需要数据是数组而且里面是对象来的,有一个name属性 lineColor 滑块颜色

3、商品列表的结构

1656212422963.png

需要一个大盒子里面一个盒子包裹,两个盒子。因为是左右结构,左边图片,右边是上下结构。这样方便写样式。 ``

<!-- 3. 商品列表 -->
    <view class="goods-list">
      <!-- 商品列表渲染  在里面循环,外面没有盒子就会很难写样式了,结构也会有问题 -->
      <view class="goods-item">
        <!-- 商品图片 -->
        <view>
          <image src="" mode="scaleToFill" />
        </view>
        <!-- 商品信息 -->
        <view class="goods-info">
          <view></view>
          <text></text>
        </view>
 </view>

v-for遍历

 <!-- 3. 商品列表 -->
    <view class="goods-list">
      <!-- 商品列表渲染  在里面循环,外面没有盒子就会很难写样式了,结构也会有问题 -->
      <view class="goods-item" v-for="v in goodsList" :key="v.goods_id">
        <!-- 商品图片 -->
        <view>
          <image :src="v.goods_small_logo" mode="scaleToFill" />
        </view>
        <!-- 商品信息 -->
        <view class="goods-info">
          <view>{{ v.goods_name }}</view>
          <text>{{ v.goods_price }}</text>
        </view>
      </view>
      
      <style lang='scss' scoped>
.goods-list {
  background-color: #f3f5f7;
  padding-top: 20rpx;
  .goods-item {
    display: flex;
    margin: 20rpx;
    border: 1px solid #f3f5f7;
    background-color: #fff;
    image {
      width: 191rpx;
      height: 191rpx;
    }
    .goods-info {
      flex: 1;
      font-size: 24rpx;
      margin-left: 20rpx;
      display: flex;
      justify-content: space-evenly;
      flex-direction: column;
      .goods-name {
            // 多行超出隐藏
        overflow: hidden;
        -webkit-line-clamp: 2;
        text-overflow: ellipsis;
        display: -webkit-box;
        -webkit-box-orient: vertical;
      }
      .goods-price {
        color: #ea4350;
        &::before {
          content: "¥";
          margin-right: 5rpx;
        }
      }
    }
  }
}
</style>

下拉加载

小程序里面有一个生命周期 // 上拉触底事件,常用于加载分页数据 onReachBottom

我们一开始定义了页面参数,页面有数,还有页码。所以我们直接页码 +1 就好了,在调用这个函数就好了

  onReachBottom() {
    this.goodData.pagenum += 1;
    this.getGoodsList();
  },

下拉加载优化:

我们数据没有了,就有提示用户(用到的是uView的Notify 消息提示 ref调用 )。以及在界面显示出来没有数据可以加载的提示

1656227661666.png 我们每次push数组长度就会增加,那就是说我们不用直接计算了。通过判断数组长度就可以是否大于等于这个就可以知道数据是否加载完了。当数据没了,之后就不用在向服务器发起请求了。

 <!-- 这个是信息提示因为小程序是不能JS直接调用信息提示,需要HTML结构的 -->
    <u-notify ref="uNotify" message="数据已经加载完了"></u-notify>
    <!-- 这里是显示底部数据加载完毕出现的 -->
    <view v-if="goodsList.length === total" class="view"
      >---我也是有底线的---</view
    >
JS
 // 上拉触底事件,常用于加载分页数据
  onReachBottom() {
    console.log(this.total);
    if (this.goodsList.length >= this.total)
      return this.$refs.uNotify.show({
        top: 10,
        type: "primary",
        color: "#fff",
        bgColor: "#eb4450",
        message: "数据已经加载完了",
        duration: 1000 * 1,
        fontSize: 20,
        safeAreaInsetTop: true,
      });
    this.goodData.pagenum += 1;
    console.log(this.goodData.pagenum);
    this.getGoodsList();
  },

上拉加载

要上拉加载,重新这个上拉的动作

"path": "pages/goods_list/goods_list",
			"style": {
				// 下拉刷新是页面的,全局的是false。前面要下拉才能监听这个事件
				"enablePullDownRefresh": true,
				"navigationBarTitleText": "商品列表"
			}
                        
           async onPullDownRefresh() {
    // 这里是要清空数组,赋值时候要给一个空数组不然会报错的。
    // 因为我们在数据时候使用的是push方法 , 我们这里不给空数组。就会报错用不了这个方法
    this.goodsList = [];
    this.goodData.pagenum = 1;
    // 因为下面是发起请求方法,要等着请求回来之后在进行下一步处理.首页是等着三个函数回来所以要用Promise.all方法这里不用
    await this.getGoodsList();
    // 提示用户
    this.$refs.uNotify.show({
      top: 10,
      type: "primary",
      color: "#fff",
      bgColor: "#5ac725",
      message: "已更新最新数据",
      duration: 1000 * 1,
      fontSize: 20,
      safeAreaInsetTop: true,
    });
    // 所有请求完成后,隐藏下拉刷新
    uni.stopPullDownRefresh();
  },

商品详情

效果图

1656230833335.png 1、准备页面:goods_detail.vue,配置路径

{
			"path": "pages/goods_detail/goods_detail",
			"style": {
				"navigationBarTitleText": "商品详情页面",
				"enablePullDownRefresh": false
			}
		},

2、跳转页面,看接口文档需要什么参数

参数名必选类型说明
goods_idnumber商品id
<navigator   把view换成navigator就好了
        :url="`/pages/goods_detail/goods_detail?goods_id=${v.goods_id}`"
        class="goods-item"
        v-for="v in goodsList"
        :key="v.goods_id"
>
  <!-- 商品图片 -->
        <image :src="v.goods_small_logo || defaultImage" mode="scaleToFill" />
        <!-- 商品信息 -->
        <view class="goods-info">
          <view class="goods-name">{{ v.goods_name }}</view>
          <text class="goods-price">{{ v.goods_price }}</text>
        </view>
      </navigator>

3、配置请求函数

// 商品详情
export function getGoodsDetailById(data) {
    return request({
        url: '/goods/detail',
        data
    })
}

4、在onLoad发起请求获取数据初始化,同时onLoad可以接收传过来的参数

<script>
import { getGoodsDetailById } from "@/api/goods";
export default {
  data() {
    return {
      goods_id: "",
      goodsDetail: {},
    };
  },
  onLoad(query) {
    // 页面加载时触发。一个页面只会调用一次,
    // 可以在 onLoad 的参数中获取打开当前页面路径中的参数。
    this.goods_id = query.goods_id;
    this.getGoodsDetail();
  },
  methods: {
    async getGoodsDetail() {
      const res = await getGoodsDetailById({ goods_id: this.goods_id });
      this.goodsDetail = res.data.message;
      console.log(this.goodsDetail);
    },
  },
};
</script>

5、准备轮播图,根据数据渲染出来

<swiper indicator-dots circular>
      <swiper-item v-for="v in goodsDetail.pics" :key="v.goods_id">
        <image :src="v.pics_mid" mode="scaleToFill" />
      </swiper-item>
    </swiper>

6、隐藏头部导航

navigationStylestringdefault导航栏样式,仅支持以下值: default 默认样式 custom 自定义导航栏,只保留右上角胶囊按钮。
设置轮播图的样式

根据设计稿来设置宽高

.goods-swiper {
  width: 750rpx;
  height: 750rpx;
  image {
    width: 100%;
    height: 100%;
  }
}

1656233852259.png

这里就需要一个返回箭头了,在vue里面router支持返回上一级的: this.$router.back()

小程序里面同样要支持 ( 跳转方式 ) open-type="navigateBack"

<navigator class="after" open-type="navigateBack">
 这里是正常rpx这个单位的,可以自适应
        <u-icon name="arrow-left" color="#fff" size="28rpx"></u-icon>
 </navigator>
 
 CSS
 <style lang='scss' scoped>
.goods-swiper {
  width: 750rpx;
  height: 750rpx;
  position: relative;
  .after {
    z-index: 99;
    background-color: rgba(0, 0, 0, 0.5);
    color: #fff;
    position: absolute;
    top: 50rpx;
    left: 30rpx;
    width: 60rpx;
    height: 60rpx;
    border-radius: 30rpx;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  image {
    width: 100%;
    height: 100%;
  }
}
</style>

预览图片

uni.previewImage(OBJECT) 调用是微信里面的底层接口

currentString/Number详见下方说明详见下方说明 这个是预览图片
urlsArray需要预览的图片链接列表
indicatorString图片指示器样式,可取值:"default" - 底部圆点指示器; "number" - 顶部数字指示器; "none" - 不显示指示器。

这是一个JS调用的,那我们需要绑定点击事件。给图片绑定点击事件,通过点击事件传入当前显示的是那个图片。

这样第一个参数就完成了,第二个参数是一个数组里面都是字符串来的。

结构布局

1656243427583.png 轮播图下面是价格,分享以及收藏这些,还有标题。

结构一个盒子包裹这些,图文详情这些是另一个盒子。

上下结构,上面是价格、下面是标题

上面价格也是左右结构,左边的价格,右边的分享收藏

小程序里面button是封装过的,可以实现分享功能 open-type="share" 其他东西都不能实现的,所以我们可以搞一个按钮定位到分享图标哪里,透明度为0就好了。display: none; 就会点击不到那个按钮的。

 <view class="goods-info">
      <view class="goods-info-title">
        <view class="goods-info-price">{{ goodsDetail.goods_price }}</view>
        <view class="goods-info-rigth">
          <view class="goods-info-button">
            <!-- 分享按钮可通过定位放到图标的上方,透明度设置为0 -->
            <u-icon name="share-square" size="44rpx"></u-icon>
            <!-- 小程序没有直接提供分享的 api,页面中需通过按钮组件实现分享功能 -->
            <button open-type="share"></button>
          </view>
          <view>
            <u-icon name="star" size="44rpx"></u-icon>
          </view>
        </view>
      </view>
      <view class="goods-info-name"> {{ goodsDetail.goods_name }} </view>
    </view>
    
    .goods-info {
  padding: 20rpx;
  .goods-info-title {
    display: flex;
    justify-content: space-between;
    margin-bottom: 20rpx;
    .goods-info-price {
      font-size: 36rpx;
      color: #ea4350;
      &::before {
        content: "¥";
        font-size: 80%;
        margin-right: 5rpx;
      }
    }
    .goods-info-rigth {
      display: flex;
      .icon-group {
        width: 44rpx;
        height: 44rpx;
      }
      .goods-info-button {
        position: relative;
        margin-right: 20px;
        button {
          position: absolute;
          right: -8rpx;
          top: -16rpx;
          opacity: 0;
        }
      }
    }
  }
}

图文详情

因为小程序是无法识别div的需要通过 富文本 进行转换
<view class="goods_introduce">
      <view style="text-align: center; margin-bottom: 20rpx">图文详情</view>
      <!-- 富文本组件展示html标签 rich-text -->
      <rich-text :nodes="goodsDetail.goods_introduce"></rich-text>
</view>

图文详情优化:富文本

1656293228430.png

在这里会出现空白的,是什么原因呢?

1656294735746.png 1、图片和文字会有一个对齐方式是基线对齐。这个就是产生白色空白的原因了!这个空白是给文字的

2、图片是行内块元素,不管你有没有文字都会留着空白给你。解决法案是转换块元素,不是最终解决法案

但是回来数据都是 div 标签,是通过富文本进行渲染出来的。所以不能直接使用给富文本添加class名从而控制样式

有没有一种方法给每一个img添加样式名字呢?

有replace,字符串的方法里面两个参数( 目标字符串, 替换的字符串 )

富文本里面有一个方法 nodes 就是和v-html一样绑定 字符串 渲染出来里面的标签

在数据回来之后修改里面的字符串给每一个字符串添加一个样式名,在改基线对齐方式就好了

 async getGoodsDetail() {
      const res = await getGoodsDetailById({ goods_id: this.goods_id });
      this.goodsDetail = res.data.message;
      this.goodsDetail.goods_introduce =
        this.goodsDetail.goods_introduce.replace(/<img/g, '<img class="img"');
      console.log(this.goodsDetail);
    },
     
        然后添加css
 .img {
  vertical-align: middle;
}

1656296240998.png

底部处理

这里联系客服,功能微信小程序的button有这个对应的功能 (open-type 微信开放能力 "contact" 打开客服会话,)。和之前一样搞一个按钮定位到上面就好了。

这里购物车和联系客服都是icon图标以及文字,可以用 flex 改变主轴方向 以及 侧轴居中就好了。

这两个按钮是我们写的样式来的,如果用微信小程序的有点麻烦。所以我们自己手写一个。

 <view class="bottom-bar">
      <view class="icon-group">
        <u-icon name="kefu-ermai" top="2rpx"></u-icon>
        <view class="kf">联系客服</view>
        <button open-type="contact">联</button>
      </view>
      <!-- 因为购物车是底部导航栏的所以要跳转方式(open-type)要设置switchTab -->
      <navigator
        url="/pages/cart/cart"
        open-type="switchTab"
        class="icon-group"
      >
        <u-icon name="shopping-cart" color="#333" size="40rpx"></u-icon>
        <view>购物车</view>
      </navigator>
      <view class="btn">加入购物车</view>
      <view class="btn">立即购买</view>
    </view>
    
    css
.bottom-bar {
  position: fixed;
  bottom: 0;
  // 这里小细节,定位之后脱离文档流了。就要设置高度,可以通过同时设置rightleft。就可以自动补充
  right: 0;
  left: 0;
  height: 94rpx;
  background-color: #fff;
  display: flex;
  justify-content: space-around;
  align-items: center;
  padding: 0 30rpx;
  .icon-group {
    flex: 1;
    position: relative;
    font-size: 22rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    .kf {
      margin-top: 6rpx;
    }
    button {
      position: absolute;
      bottom: -36rpx;
      padding-bottom: 20rpx;
      opacity: 0;
    }
  }
  .btn {
    width: 196rpx;
    height: 60rpx;
    color: #fff;
    background-color: #ea4350;
    border-radius: 30rpx;
    text-align: center;
    line-height: 60rpx;
    margin: 10rpx;
    font-size: 26rpx;
  }
  view:nth-child(3) {
    background-color: #fcaa23;
  }
}

购物车

商品数据是多个页面共同使用的,vue里面可以使用vuex但是小程序是不支持的。uniapp是封装过了,是支持vuex的。

1656298517785.png

使用vuex的步骤

// 引入
import Vue from 'vue';
import Vuex from 'vuex';
// 以插件形式注册到 Vue 中
Vue.use(Vuex)
// 实例化store
const store = new Vuex.Store({
    // 初始化数据
    state: {
        count: 11
    },
    getters: {
        // 这个相当于vue里面计算属性,一般用于解构方便我们后面调用。这个参数只能是初始化数据的
        doubleCount(state) {
            return state.count * 2
        }
    },
    mutations: {
        // 要修改state只能通过同步方法修改。这个参数只能是初始化数据的
        addCount(state, count) {
            state.count += count
        }
    },
    actions: {
        asyncAdd(store, count) {
            setTimeout(() => {
                // 一般用于发起请求接收数据,调用同步方法进行修改
                // 模拟请求完成后修改。这个参数整一个store对象可以调用同步方法还有计算属性
                store.commit('addCount', count)
            }, 1000)
        }
    }
})
// 导出这个  store 对象到  实例化  vue  里面注册
export default store

main.js
import store from './store';
const app = new Vue({
	...App,
	store
})

<template>
  <view>
    cart
    <view>Vuex全局计数器 {{ $store.state.count }}</view>
  </view>
</template>

1656299172722.png

虽然支持vuex但是不完全支持( 不支持插值表达式的 ),JS 里面可以获取到vuex的属性的。需要一个变量装起来,那么一个属性需要依赖另一个属性,在vue里面是有一个计算属性的。也可以通过通过 mapState 辅助函数获取。

这里为什么不用变量呢?data定义的变量是页面一刷新获取到的数据,就是什么数据不会改变,计算属性会时刻刷新。

cart.vue
<template>
  <view>
    cart
    <view>Vuex全局计数器 {{ $store.state.count }}</view>
    <view>{{ count }}</view>
    <view>{{ doubleCount }}</view>
    <button @click="asyncAdd">+2</button>
    <button @click="addBtn">+1</button>
  </view>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count;
    },
    doubleCount() {
      // 因为vuex的计算属性是不能传参数进去,vuex计算属性是getters这个对象后面的。自己定义属性名
      vuex的计算属性  直接调用就好了
      return this.$store.getters.doubleCount;
    },
  },
  data() {
    return {};
  },
  methods: {
    addBtn() {
      // 同步调用是通过commit这个方法里面这个方法里面两个参数,一个是定义函数名,另一个是参数
      this.$store.commit("addCount", 1);
    },
    asyncAdd() {
      // 异步调用是通过dispatch这个方法里面两个参数,一个是定义函数名,另一个是参数
      this.$store.dispatch("asyncAdd", 1);
    },
  },
};
</script>

我们先整一个简单的vuex购物车,这里 操作用的是 uView 里面的 NumberBox 步进器

1656312961304.png

<template>
  <view>
    <u-search placeholder="请输入商品" v-model="keyword"></u-search>
    <view class="title">
      <view class="item">商品</view>
      <view class="item">操作</view>
    </view>
    <view class="content" v-for="v in list" :key="v.goods_id">
      <view class="item">{{ v.goods_name }}</view>
      <view class="item">
        <u-number-box v-model="v.goods_count"></u-number-box>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  computed: {
    list() {
      return this.$store.state.list;
    },
  },
  data() {
    return {
      keyword: "",
    };
  },
};
</script>

<style lang="scss">
.title,
.content {
  display: flex;
  .item {
    flex: 1;
    border: 1rpx solid #ddd;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20rpx;
  }
}
</style>


Vuex

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex)

const store = new Vuex.Store({
    state: {
        list: [
            {
                goods_id: 1,
                goods_name: '手机',
                goods_count: 1
            },
            {
                goods_id: 2,
                goods_name: '电脑',
                goods_count: 2
            }
        ]
    }
})

export default store

模拟商品添加到购物车的情况

小程序里面点击输入框都是有键盘弹出来的, uView 里面的 Search 搜索

事件

search 用户确定搜索时触发,用户按回车键,或者手机键盘右下角的"搜索"键时触发

custom 用户点击右侧控件时触发

mutations: {
        addList(state, list) {
            state.list = list
        }
    }
    
     methods: {
    handlerSearch() {
      // 在vuex里面是直接覆盖了之前存储的数据,所以我们这里返回数据要有之前的数据在里面
      // const arr = this.list  如果是这样赋值就会,出现深拷贝的情况。vuex修改数据只能通过mutations这样是不允许的,遵守单向数据流
      const arr = [...this.list]; //我们给一个新的数组和vuex的数组没有关系了
      arr.push({
        goods_id: Date.now(), //我们给一个新的数组和vuex的数组没有关系了
        goods_name: this.keyword,
        goods_count: 1,
      });
      this.$store.commit("addList", arr);
      this.keyword = "";
    },

优化:

添加里面已经有的商品,应该是数量+1.而不是覆盖之前的数据。

<u-search
      placeholder="请输入商品"
      v-model="keyword"
      @search="handlerSearch"
      @custom="handlerSearch"
      actionText="添加"
    ></u-search>
    
     handlerSearch() {
      // 在vuex里面是直接覆盖了之前存储的数据,所以我们这里返回数据要有之前的数据在里面
      // const arr = this.list  如果是这样赋值就会,出现深拷贝的情况。vuex修改数据只能通过mutations这样是不允许的,遵守单向数据流
      const arr = [...this.list]; //我们给一个新的数组和vuex的数组没有关系了

      // 添加里面已经有的商品,应该是数量+1.而不是覆盖之前的数据。
      // 添加商品的名字会数组里面的每一个名字进行匹配,在后面用ID进行匹配的 ,find 返回匹配成功的对象
      const index = arr.find((v) => v.goods_name === this.keyword);

      if (index) {
        // 判断有这个对象,这个对象的数量++。因为遍历出来的对象是vuex里面的数组的对象是复杂数据类型
        // 之间是有联系的,修改这个也会修改数组里面的
        index.goods_count++;
      } else {
        // 没有就添加进去
        arr.push({
          goods_id: Date.now(), //我们给一个新的数组和vuex的数组没有关系了
          goods_name: this.keyword,
          goods_count: 1,
        });
      }
      // 不管是添加商品还是添加数量都要重新赋值vuex的,这个步骤就不会在if  else里面了
      this.$store.commit("addList", arr);
      this.keyword = "";
    },

添加商品数量

1、NumberBox 步进器 文档方法

change输入框内容发生变化时触发,对象形式
value:输入框当前值name 步进器标识符,在change回调返回
这里如果绑定了,     value和 name 就可以不用传参数这个方法内部会帮我们传参数进去的
      <u-number-box
          :value="item.goods_count"
              :name="item.goods_id"
          @change="change"
        ></u-number-box>
        这里进行一个解构会方便一点
       change({value,name }) {
      const arr = [...this.list];
      // 这里是通过ID找到数组里面对应的对象,使用findIndex匹配到返回是索引
      const index = arr.findIndex((v) => v.goods_id === name);
      // 通过这个索引把这个数量进行覆盖,
      arr[index].goods_count = value;
      this.$store.commit("addList", arr);
    },   

2、vue里面的方法

 <!-- 
          组件 v-model 本质是一个语法糖:
          原理:名为 value 的属性和名为 input 的事件组合而成

          @input="handlerNumberInput"
            没有写小括号时 vue 帮我传了 $event,等价于以下代码:
          @input="handlerNumberInput($event)"
            注意:自定义组件 $event 就是组件内部回传的数据
         -->

        <!-- <u-number-box
          :value="v.goods_count"
          @input="handlerNumberInput($event, v.goods_id)"
        ></u-number-box> -->
        
        
          // handlerNumberInput(val, id) {
    //   const arr = [...this.list];
    //   const index = arr.findIndex((v) => v.goods_id === id);
    //   arr[index].goods_count = val;
    //   this.$store.commit("addList", arr);
    // },

本地存储

uni.setStorageSync(KEY,DATA)

将 data 存储在本地缓存中指定的 key 中,会覆盖掉原来该 key 对应的内容,这是一个同步接口。

参数说明

参数类型必填说明
keyString本地缓存中的指定的 key
dataAny需要存储的内容,只支持原生类型、及能够通过 JSON.stringify 序列化的对象
  mutations: {
        // 我们是同步修改了vuex里面的数据随便本地存储的
        addList(state, list) {
            state.list = list
            // 小程序数据缓存 相当于 Web端的本地存储
            //   uni.setStorageSync('key', 'val')
            //     类似于 localStorage.setItem('key','val')
            //   uni.getStorageSync('list')
            //     类似于 localStorage.getItem('list')
            // vue本地存储里面的对象是要   JSON.stringify  对象转换字符串
            // vue读取本地存储里面的对象是要   JSON.parse  字符串转换对象
            // 🎯注意:小程序本地存储可直接保存对象和数组,不需要转换和解析JSON字符串
            // 我们传过来的参数已经是修改完了的,包含了之前的数据的。直接用这个数据本地存储就好了
            uni.setStorageSync('storeList', list)
        }
    }
uni.getStorageSync(KEY)

从本地缓存中同步获取指定 key 对应的内容。 参数说明

参数类型必填说明
keyString本地缓存中的指定的 key
state: {
        list: uni.getStorageSync('storeList') || []
    },
购物车布局

1656333290174.png 购物车的

1656333316074.png

商品列表的

大致的结构是一样的,我们可以把这个公共样式提取出来。放到App.vue的style里面,只要有这个样式名字就可以用这个样式了。

购物车多了,单选框已经 uView 的 NumberBox 步进器 。

我们商品列表是通过一个盒子包裹里面是导航遍历出来的,外面在套盒子包裹单选框。设置 display: flex;就可以一行在一起 align-items: center; 侧轴居中就好了

NumberBox 步进器 可以定位,也可以放在价格里面的 通过 设置 display: flex;。

<template>
  <view>
    <!-- 商品列表渲染 -->
    <view class="cart-item" v-for="item in cartList" :key="item.goods_id">
      <radio :checked="item.goods_select" color="#EB4450"></radio>
      <navigator
        class="goods-item"
        hover-class="none"
        :url="`/pages/goods_detail/goods_detail?goods_id=${item.goods_id}`"
      >
        <!-- 商品图片 -->
        <image
          class="goods-image"
          :src="item.goods_small_logo || defaultImage"
          mode="scaleToFill"
        />
        <!-- 商品信息 -->
        <view class="goods-info">
          <view class="goods-name">{{ item.goods_name }}</view>
          <view class="goods-price">
            <view class="box">
              {{ item.goods_price }}
            </view>
              
              自定义颜色和大小
通过button-size参数设置按钮大小
通过icon-style参数设置加减按钮图标的样式
            <u-number-box
              bgColor="white"
              v-model="item.goods_count"
              color="#8a8a8a"
              iconStyle="color: #8a8a8a;  font-size: 18rpx; display: flex; justify-content: center;"
            ></u-number-box>
          </view>
        </view>
      </navigator>
    </view>
  </view>
</template>

<script>
export default {
  computed: {
    cartList() {
      return this.$store.state.cartList;
    },
  },
};
</script>

<style lang="scss">
.cart-item {
  padding: 25rpx;
  display: flex;
  align-items: center;
}
.goods-price {
  display: flex;
  align-items: center;
  color: #ea4350;
  justify-content: space-between;
  .box::before {
    content: "¥";
    font-size: 80%;
    margin-right: 5rpx;
  }

  .u-number-box {
        // 细节:扩大盒子范围,防止用户误操作
    padding: 20rpx;
    text {
      width: 32rpx;
      height: 32rpx;
      border-radius: 18rpx;
      border: 1px solid #8a8a8a;
    }
  }
}
</style>
购物车功能实现

购物车里面添加商品都是从商品详情页面,点击加入购物车之后。有一个对话框,选择里面的商品参数以及颜色这些。确定之后才添加到购物车里面。

所以我们的功能是在商品详情里面添加之后,才能出现在购物车里面的。

1656396948912.png

这里面的都是我们要在vuex里面存储的信息,这里就有五个信息了。还有最关键的一个是商品的ID,如果没有商品ID就无法跳转到商品详情页面。也无法下单。等等一系列的操作都是基于商品的ID之上实现的。

// 初始化购物车数据
        cartList: [
            {
                // 商品id
                goods_id: '',
                // 商品图片
                goods_small_logo: '',
                // 商品名称
                goods_name: '',
                // 商品价格
                goods_price: '',
                // ---- 用户能操作改变的字段
                // 商品数量
                goods_count: '',
                               // 商品选中状态,为什么这里的商品选中状态是true呢?
                // 因为刚刚加入购物车的商品往往是要购买的,所以选中状态是true
                goods_select: true
            }
        ]
    },

在商品详情页面的加入购物车绑定点击事件:<view class="btn" @click="handlerAddCart">加入购物车

打印vuex里面的最开始的数据以及axios获取到的商品信息,看看是否触发。

 handlerAddCart(){
      console.log(this.goodsDetail);
      console.log(this.$store.state.cartList);
    }

然后就是感觉vuex里面对象进行信息填入

handlerAddCart(){
      const arr= [...this.$store.state.cartList]
      arr.push( {
                // 商品id
                goods_id: this.goodsDetail.goods_id,
                // 商品图片
                goods_small_logo:this.goodsDetail.goods_small_logo,
                // 商品名称
                goods_name:this.goodsDetail. goods_name,
                // 商品价格
                goods_price:this.goodsDetail.goods_price,
                // ---- 用户能操作改变的字段
                // 商品数量,一般是一个。如果还有对话框就可以在对话框里面选择数量了
                goods_count:1,
                // 商品选中状态,为什么这里的商品选中状态是true呢?
                // 因为刚刚加入购物车的商品往往是要购买的,所以选中状态是true
                goods_select: true
            })
      this.$store.commit('setCartList',arr)
      console.log(this.$store.state.cartList);
    }

const store = new Vuex.Store({
    state: {
        // 初始化购物车数据
        cartList: [ ]
    },
    mutations:{
        setCartList(state,cartList){
            state.cartList=cartList
        }
    }
})
优化:

就是同样的商品添加购物车应该是添加数量,而不是重新两个一模一样的商品。

 // findIndex和find区别
      // findIndex匹配成功返回的是索引,都没有就返回的是-1
    // find返回的是这个对象
    const index= arr.findIndex(v=>v.goods_id===this.goodsDetail.goods_id)
    // -1转换布尔值是true,所以这里判断条件是 index>=0 因为索引有可能是0
     if(index>=0){
         arr[index].goods_count++
        console.log(index);
     }else{
       arr.push( {
                // 商品id
                goods_id: this.goodsDetail.goods_id,
                // 商品图片
                goods_small_logo:this.goodsDetail.goods_small_logo,
                // 商品名称
                goods_name:this.goodsDetail. goods_name,
                // 商品价格
                goods_price:this.goodsDetail.goods_price,
                // ---- 用户能操作改变的字段
                // 商品数量,一般是一个。如果还有对话框就可以在对话框里面选择数量了
                goods_count:1,
                // 商品选中状态,为什么这里的商品选中状态是true呢?
                // 因为刚刚加入购物车的商品往往是要购买的,所以选中状态是true
                goods_select: true
            })
     }

提示信息

用户添加完了之后要有提示信息。

这里使用 uni-app 的 uni.showToast

这个添加到 js 的 icon 默认参数是 success 用户可能在提示信息时候,会再一次点击就会。多次添加商品

showToast的 mask 是否显示透明蒙层,防止触摸穿透,默认:false 就可以解决这个问题

uni.showToast({
        title: "添加成功",
        duration: 2000,
        mask: true,
 });

本地存储

在vuex改变了 state里面的商品信息时候,随便进行本地存储。

 state: {
        // 初始化购物车数据,购物车是空的,那么本地存储当前就是 undefined  在后面数据进行操作可能会出现错误,为了解决这个办法我们添加了一个空数组。
        cartList: uni.getStorageSync('cartList') || []
    },
    mutations: {
        setCartList(state, cartList) {
            state.cartList = cartList
            uni.setStorageSync('cartList', cartList)
        }
    }

最后是修改数量添加到vuex里面

<!-- 底部 -->
    <view class="bottom-bar">
      <radio
        class="select"
        value="全选"
        :checked="goods_select"
        @click="changeSelectAll"
      />
      <view class="total"
        >合计:<text>{{ selectCartListPrice }}</text></view
      >
      <view
        :style="{
          backgroundColor: selectCartListCount === 0 ? '#e7414f5e' : '#ea4350',
        }"
        class="account"
        @click="btnOK"
      >
        去结算({{ selectCartListCount }})
      </view>
    </view>
    
      isSelectAll(state, getters) {
            console.log(state, getters);

            return state.cartList.every(v => v.goods_select)
        },

vuex里面的计算属性可以接收两个参数的,打印发现一个是state另一个是整一个vuex的计算属性来的

1656473248692.png

支付页面

效果图

1656502707445.png

1656502935466.png

结构分析

通过效果图可以看出来上面是按钮,选择地图然后之后隐藏按钮。显示地址通过 vue 的 v-if ,这里获取对象我们用 uni-app 的 uni.chooseAddress

获取用户收货地址。调起用户编辑收货地址原生界面,并在编辑完成后返回用户选择的地址,需要用户授权 scope.address。 是一个Promise封装的。可以使用 async await

success返回参数说明

属性类型说明平台差异说明
userNamestring收货人姓名
postalCodestring邮编
provinceNamestring国标收货地址第一级地址
cityNamestring国标收货地址第二级地址
countyNamestring国标收货地址第三级地址
detailInfostring详细收货地址信息
nationalCodestring收货地址国家码
telNumberstring收货人手机号码
errMsgstring错误信息微信小程序
<view class="address" @click="getUserAddress">
  </view>
  
   async getUserAddress() {
      const [error, res] = await uni.chooseAddress();
      console.log(res);
      this.address = res;
    },

可以看出来这里得到的数据太散了,要做到效果图那样子就需要重新拼接起来。这里可以用计算属性,一个值是根据另一个值得到结果。

computed: {
    addressDetail() {
      const { provinceName, cityName, countyName, detailInfo } = this.address;
    //   第一种是普通写法最简单的
      // return provinceName+cityName+countyName+detailInfo
    //   join把数组拼接成为字符串函数里面的参数是  以什么拼接,我们这里是以空格拼接的
      return [provinceName, cityName, countyName, detailInfo].join(" ");
    },
}

得到数据之后就可以把数据渲染到页面上

 <view class="address" @click="getUserAddress">
      <view class="address-info" v-if="address.cityName">
        <view>{{ addressDetail }}</view>
        <view class="address-user"
          >{{ address.userName }} <text>{{ address.telNumber }}</text></view
        >
      </view>
      <view v-else class="address-btn">选择地址</view>
    </view>

下面的那些数据是根据购物车里面选中的商品,拿到支付页面去渲染的

之前我们就把选中的商品通过 filter 提前出来了封装在 vuex的 计算属性里面,直接使用 vuex 的计算属性 就可以得到里面的结果了。下面的数量,总价。我们之前都封装过了,一个一个封装成为 vuex 的计算属性太麻烦了。vuex 提供了一个辅助函数 ,mapGetters 函数 参数 是要传过去一个数组进去 在拓展出来,就可以得到vue的计算属性了。

import { mapGetters } from "vuex";
computed: {
    ...mapGetters([
      "selectCartList",
      "selectCartListPrice",
      "selectCartListCount",
    ]),
  },
  
   <!-- 3. 商品列表 -->
    <view class="goods">
      <!-- 商品列表渲染  在里面循环,外面没有盒子就会很难写样式了,结构也会有问题 -->
      <navigator
        :url="`/pages/goods_detail/goods_detail?goods_id=${v.goods_id}`"
        class="goods-item"
        v-for="v in selectCartList"
        :key="v.goods_id"
        style="border: 1px solid #f3f5f7"
      >
        <!-- 商品图片 -->
        <image :src="v.goods_small_logo || defaultImage" mode="scaleToFill" />
        <!-- 商品信息 -->
        <view class="goods-info">
          <view class="goods-name">{{ v.goods_name }}</view>
          <text class="goods-price">{{ v.goods_price }}</text>
        </view>
      </navigator>
    </view>

底部是和购物车一样的代码

 <!-- 底部 -->
    <view class="bottom-bar">
      <view class="total"
        >合计:<text>{{ selectCartListPrice }}</text></view
      >
      <view class="account" @click="handlerPay">
        去支付({{ selectCartListCount }})
      </view>
    </view>

上面的样式我提取到,app.vue里面了

支付页面的重点、难点

首先要登录 和 选择收货地址 ,才能去支付(我们这里是微信小程序,所以调用微信的支付接口)

1656505058933.png 我们要先解决登录的问题!

在微信小程序里面登录是点击登录之后,有一个对话框。提示你是否授权登录。同意之后返回给你微信登录的凭证

在我们这里是登录之后得到token,存储到vuex里面。最后是本地存储,数据持久化

在vuex 设置 登录信息

state: {
        userProfile: uni.getStorageSync('userProfile') || {}
    },

mutations: {
        setUserProfile(state, userProfile) {
            state.userProfile = userProfile
            uni.setStorageSync('userProfile', userProfile)
        }
}

准备就绪之后看接口文档

参数是要

参数名必选类型参数说明
encryptedDatastring执行小程序 获取用户信息后 得到
rawDatastring执行小程序 获取用户信息后 得到
ivstring执行小程序 获取用户信息后 得到
signaturestring执行小程序 获取用户信息后 得到
codestring执行小程序登录后获取

这些都是 在 微信 登录之后 才能 获取到的,我们微信登录是相当于得到账号密码,最终拿到账号密码发起请求后台登录,得到token的。

登录准备

uni.getUserInfo ( 获取用户信息 )

success 返回参数说明

image.png

这里就有我们要的四个参数了(encryptedData, rawData, signature, iv)

async handlerPay() {
const [err,res]=await uni.getUserInfo()
if(res){
    const {encryptedData, rawData, signature, iv} =res
}
}

这里还差一个参数就是 code 这个是在 uni.login 登录 才能得到

async handlerPay() {
const [loginError, loginRes]=await uni.login()
if(res){
    const {code} =loginRes
}
}

准备完了之后可以发起后台登录了,但是后台登录是要对应的 appId 的 这个appId是企业的。总不能随便就可以登录企业的就会有不安全的问题

所以我是向我们的公司负责账号的人要到 公司的 appId,并且 公司把我的微信号添加到了开发成员哪里。才能使用的不然会报错的。

登录请求

我们是在vuex里面的异步发起请求的,

actions: {
        async userWxlogin(store, data) {
            const obj = await userWxlogin(data)
            store.commit('setUserProfile', obj)
        }
    }

后面的支付流程都是需要我们在后台登录之后返回的token的,需要我们在请求拦截器里面的请求头设置token的

这个需要请求头的我们和后台讨论过了,如果需要调用token的接口 后面都会有 my 作为标识符的。我们可以通过判断有没有 my 从而判断是否需要请求头。

es 6 里面有一个新的 方法 includes

includes() 方法用于判断字符串是否包含指定的子字符串。

如果找到匹配的字符串则返回 true,否则返回 false。

注意: includes() 方法区分大小写。

当时封装时候,我们已经封装了基本路径了。只需要判断传过来的路径有没有 my 就好了

有就证明需要token的,如果是判断整一个基本路径可能会有问题的。

if (config.url.includes('/my')) {
        config.header = {
            // 如果有就从本地存储获取也可以是在vuex里面获取
            Authorization: uni.getStorageSync('userProfile').token
        }
    }

因为整一个 登录 流程在 用户页面也是需要用到的

所以我把这个封装到 vuex里面去了

把整一个登录流程方法放到 vuex的 actions 这样方便复用

async userWxLogin(store) {
 const [userInfoError, userInfoRes] = await uni.getUserInfo();
            const { encryptedData, rawData, signature, iv } = userInfoRes;

            const [loginError, loginRes] = await uni.login();
            const { code } = loginRes;
            const data = {
                encryptedData,
                rawData,
                signature,
                iv,
                code,
            };
            console.log(data);
            const obj = await userWxlogin(data)
            store.commit('setUserProfile', obj)
}

支付流程

1656505043669.png 先看接口文档

参数名必选类型说明
order_pricestring订单总价格
consignee_addrstring收货地址
goodsArray订单数组
这里是要求要收货地址的,所以我们需要判断有没有地址才能进行。

创建订单之前准备

判断有没有地址没有就结束函数,提示用户

 // Object.keys是把对象里面的属性名全部返回出来是一个数组的形式的
      if (Object.keys(this.address).length === 0) {
        return uni.showToast({ title: "请选择收货地址~", icon: "none" });
      }

判断有没有登录,因为我们把整一个的登录信息存储到vuex里面所以可以直接使用刚刚这个方法。

 if (Object.keys(this.$store.state.userProfile).length === 0) {
        return uni.showToast({ title: "没有登录,请先您登录~", icon: "none" });
      }

创建订单

这里请求的里面有一个地址,我们之前用计算属性封装过了直接调用就可以了,总价在vuex也封装过来。但是 订单数组 需要我们进行处理的。

goods字段说明

参数名必选类型说明
goods_idnumber商品id
goods_numbernumber购买的数量
goods_pricenumber单价

在调用函数时候,返回一个新的数组。map就比较合适了

 const ordersCreateRes = await ordersCreate({
        selectCartListPrice,
        consignee_addr: this.consignee_addr,
        // 选中的商品我们已经通过 vuex计算属性 里面使用  filter  筛选出来并且在上面通过辅助函数引入直接调用就好了
        goods: this.selectCartList.map(
          ({ goods_id, goods_price, goods_count }) => ({
            goods_id,
            goods_price,
            // 因为我们之前取名字是和接口文档不一样,所以我们解构时候就需要重新命名
            goods_count: goods_number,
          })
        ),
      });
      console.log(ordersCreateRes);
            const { order_number } = ordersCreateRes.data.message
      console.log('order_number', order_number)

获取支付参数

我们创建订单之后,就有返回值这个返回值就是 订单编号。在通过订单编号发起获取支付参数就可以得到后台加密过的 信息,这个后台信息微信支付是看得懂的我们看不懂的。

// 根据订单号生成订单支付参数
export function ordersPayInfo(data) {
    return request({
        url: '/my/orders/req_unifiedorder',
        method: 'POST',
        data
    })
}


const { order_number } = ordersCreateRes.data.message;
      console.log("order_number", order_number);
      //   这里如果属性名和属性值是一致就可以省略只写一个
      const payInfoRes = await ordersPayInfo({ order_number });
      const { pay } = payInfoRes.data.message;
      console.log("pay", pay);

返回参数说明

参数名类型说明
payobject该对象内的参数,为调用微信支付所必须

微信支付

uniapp也可以调用wx支付的接口但是太多了,太麻烦。还不如用原生的,并且原生的接口需要的参数我们后台已经准备好了。我们自己发起请求就好了。而且微信支付只能企业才能使用的。

wx.requestPayment(Object object)
功能描述

发起微信支付。调用前需在小程序微信公众平台 -功能 - 微信支付入口申请接入微信支付。了解更多信息,可以参考 微信支付开发文档

参数有点多去官网看吧

    // 3. 调用微信接口实现 支付
      const { errMsg } = await wx.requestPayment(pay);
      console.log("errMsg", errMsg);

更新订单支付状态

为什么要更新订单支付状态呢?

1656596522642.png

我们就需要把微信支付的返回值接收发过后端让后端去查看是否支付成功。

  // 4. 更新订单支付状态
      const ordersCheckRes = await ordersCheck(order_number);
      console.log("ordersCheckRes", ordersCheckRes);
      // 5. 跳转到订单页面
        uni.showToast({ title: '支付成功', icon: 'success' })
        
优化处理

如果用户取消支付了,那么或者是用户想支付但是红包里面又没有钱怎么办?

所以我们要进行接收错误的处理

因为是用 Promise 封装就可以接收 try { } catch (error) { } 这样接收错误的处理 ,里面只要有一个错了就会

直接终止剩下的代码,如果后台接收报错信息就可以发起请求。

 try {
 里面是上面的支付流程的代码
  }catch (error) {
        console.log("发送给后端错误", error);
        uni.showToast({ title: "支付失败", icon: "error" });
      }

打包上线

npm run dev是我们开发时候运行的,基本上没有压缩过的。如果要压缩的就需要另一个命令了,这些命令都是在JSON里面封装过的。

为什么要打包呢?

小程序目的就是为了打开快速的,上线的包不能单个分包/主包大小不能超过 2M、

开发命令和构建命令

这些命令都可以在 文件里面找到的

# 开发命令
npm run dev:mp-weixin

# 构建命令
npm run build:mp-weixin

1656758497241.png 两个命令的差别:

  1. dev 包含了调试时的代码,JS代码没有压缩,源代码修改会重新编译,用于开发。

  2. build 把所有代码有压缩了,JS代码不能调试了,但是体积小,用户打开速度更快,用于项目上线。

    在 我们 vue 项目里面想看到 打包之后谁体积大通过 npm run build:prod -- --report 看的

    小程序里面是不需要的

1656758769058.png

上传发布

在小程序中 发布项目相对于网页来说 要简单不少

  1. 发布前 先打包代码
  2. 发布前请检查是否已经填写好了正确的 appid
  3. 在微信开发者工具中 点击 上传

1656758648530.png

分包

按照微信可以分无数个包的,都是全部加起来不能超过20M也就是整个小程序所有分包大小不超过 20M

在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。

使用分包

配置方法

假设支持分包的小程序目录结构如下:

├── app.js
├── app.json
├── app.wxss
├── packageA
│   └── pages
│       ├── cat
│       └── dog
├── packageB
│   └── pages
│       ├── apple
│       └── banana
├── pages
│   ├── index
│   └── logs
└── utils

按照微信文档说法,分包是不能写在主包里面的。需要在创建一个文件夹存放分包的,每一个分包都有对应的文件夹的。

我们这里创建了 packageFruit/fruit/fruit.vue 里面分我们的分包,需要访问就必须要路由。

细节:这里创建文件最好和主包创建是一样的,方便我们迁移。

分包的路由是在 app.json 里面 subpackages 字段声明项目分包结构:并不是写在主包路由里面的

写成 subPackages 也支持。

subpackages 中,每个分包的配置有以下几项:

字段类型说明
rootString分包根目录
nameString分包别名,分包预下载时可以使用
pagesStringArray分包页面路径,相对于分包根目录
independentBoolean分包是否是独立分包

subpackages 是一个数组来的,里面是一个对象来的。

对象里面是和上面配置路由是一样的

基本上是一样的,这是多了一个root,这个root让我想到了vue的路由    二级路由
和二级路由一样要跳转也是要   拼接一级路由名字的,才能获取到二级路由的
<navigator url="/packageFruit/fruit/fruit"> 🍉去分包水果页 </navigator>

1656562086524.png

"subpackages": [
		{
			"root": "packageFruit",
			"pages": [
				{这里是不用写分包的文件夹的,直接写文件夹下面的
					"path": "fruit/fruit",
					"style": {
						"navigationBarTitleText": "分包",
					}
				}
			]
		}
	],
    基本上是一样的,这是多了一个root,这个root让我想到了vue的路由  
二级路由
    "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
		{
			"path": "pages/cart/cart",
			"style": {
				"navigationBarTitleText": "购物车页面",
				"enablePullDownRefresh": false
			}
		},
        ]

独立分包

独立分包是小程序中一种特殊类型的分包,可以独立于主包和其他分包运行。从独立分包中页面进入小程序时,不需要下载主包。当用户进入普通分包或主包内页面时,主包才会被下载。

开发者可以按需将某些具有一定功能独立性的页面配置到独立分包中。当小程序从普通的分包页面启动时,需要首先下载主包;而独立分包不依赖主包即可运行,可以很大程度上提升分包页面的启动速度。

小细节

1、小程序固定参数页面

页面是没有这个问题的,因为路径已经是固定的。不管怎么刷新都是会有的

1656235540377.png 小程序里面下次下次可能会直接跳转到首页,回到我们写页面就会点回去很麻烦。

1656235190556.png 把我们要写的页面放到数组第一个位置就好了,但是有一个问题就是没有。参数( id ) 这个ID是其他页面传过来的。每次实现都没有了,没有参数我们就请求获取不到数据了。那我们可以设置一个固定的参数

1656235256263.png

1656235299588.png

2、 uView 的安全区适配

这个适配,主要是针对IPhone X等一些底部带指示条的机型,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行底部安全区适配。

就是给一个空白的盒子,根据不同进行设置不同的高度

顶部安全区

由于我们在做页面布局时经常会使用顶部位置,uView提供了一个组件u-status-bar,如u-popup从顶部弹出时,可以考虑使用此组件。

<template>
	<view>
		<u-status-bar></u-status-bar>
		......
	</view>
</template>

3、vs code

下载的微信小程序插件,每次打开可能会偷偷的下载依赖。从而导致报错的,

1656464018571.png

4、快速生成嵌套css样式

下载 CSS Tree

1656467256259.png 可以省略我们在css写嵌套样式名的过程 搜索css tree

1656468956951.png 删除多余的样式名字

1656469048825.png

5、不同编译器的兼容问题

1656472352984.png

1656476544322.png

uni-app 里面有

跨端兼容

uni-app 已将常用的组件、JS API 封装到框架中,开发者按照 uni-app 规范开发即可保证多平台兼容,大部分业务均可直接满足。

但每个平台有自己的一些特性,因此会存在一些无法跨平台的情况。

  • 大量写 if else,会造成代码执行性能低下和管理混乱。
  • 编译到不同的工程后二次修改,会让后续升级变的很麻烦。

在 C 语言中,通过 #ifdef、#ifndef 的方式,为 windows、mac 等不同 os 编译不同的代码。 uni-app 参考这个思路,为 uni-app 提供了条件编译手段,在一个工程里优雅的完成了平台个性化实现。

条件编译

条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。

条件编译是用特殊的注释作为标记,在编译时根据这些特殊的注释,将注释里面的代码编译到不同平台。

写法: 以 #ifdef 或 #ifndef 加 %PLATFORM% 开头,以 #endif 结尾。

  • #ifdef:if defined 仅在某平台存在
  • #ifndef:if not defined 除了某平台均存在
  • %PLATFORM% :平台名称
条件编译写法说明
#ifdef APP-PLUS 需条件编译的代码 #endif仅出现在 App 平台下的代码
#ifndef H5 需条件编译的代码 #endif除了 H5 平台,其它平台均存在的代码
#ifdef H5MP-WEIXIN 需条件编译的代码 #endif在 H5 平台或微信小程序平台存在的代码(这里只有,不可能出现&&,因为没有交集)

1656492659408.png

1656492699261.png

我们在上面注释是H5的,小程序是不符合添加的所以小程序的bottom是0,H5的是50

这个不同视口变化会导致 分类页面 出现滚动条的

1656573009699.png

1656573120690.png

1656573171529.png

  /* #ifdef H5 */
    height: calc(100vh - 50px - 40px - 88rpx);
    /* #endif */
固定值

uni-app 中以下组件的高度是固定的,不可修改:

组件描述AppH5
NavigationBar导航栏44px44px
TabBar底部选项卡HBuilderX 2.3.4 之前为 56px,2.3.4 起和 H5 调为一致,统一为 50px。但可以自主更改高度)50px

各小程序平台,包括同小程序平台的 iOS 和 Android 的高度也不一样。

这里为什么是 px 呢 ? 因为在 uni-app 设置了固定值了 所以 我们模拟器这里 底部是固定 50 px的 上面的40 还有搜索框的88 而且PC端一开始的 html这些样式怎么来的 ?

1656755919722.png

uni-app #CSS 变量

全局样式与局部样式

定义在 App.vue 中的样式为全局样式,作用于每一个页面。在 pages 目录下 的 vue 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 App.vue 中相同的选择器。

注意:

  • App.vue 中通过 @import 语句可以导入外联样式,一样作用于每一个页面。
  • nvue 页面暂不支持全局样式

uni-app 提供内置 CSS 变量

CSS 变量描述App小程序H5
--status-bar-height系统状态栏高度系统状态栏高度 (opens new window)、nvue 注意见下25px0
--window-top内容区域距离顶部的距离00NavigationBar 的高度
--window-bottom内容区域距离底部的距离00TabBar 的高度

这些内容都是随便设置的吗?是根据 系统信息的概念 在里面函数的返回值设置的

#系统信息的概念

uni-app提供了异步(uni.getSystemInfo)和同步(uni.getSystemInfoSync)的2个API获取系统信息。

系统信息返回的内容非常多,各操作系统、各家小程序、各浏览器对它们的定义也不相同。uni-app里重新梳理了这些概念,同时为了向下兼容也保留了这些平台原来的概念,但不推荐使用。

按照运行环境层级排序,从底层向上,uni-app有6个概念:

  • device:运行应用的设备,如iphone、huawei
  • os:设备的操作系统,如 ios、andriod、windows、mac、linux
  • rom:基于操作系统的定制,Android系统特有概念,如miui、鸿蒙
  • host:运行应用的宿主程序,即OS和应用之间的运行环境,如浏览器、微信等小程序宿主、集成uniMPSDK的App。uni-app直接开发的app没有host概念
  • uni:uni-app框架相关的信息,如uni-app框架的编译器版本、运行时版本
  • app:开发者的应用相关的信息,如应用名称、版本
onLaunch: function () {
    const res = uni.getSystemInfoSync()
    console.log('res', res);
  },

我们调用这个方法打印出来看看

1656756393051.png 这里就不进行深入研究了。

也就是说这个方法根据不同的游览器就会返回不同的返回对应的 宽度、高度这些,我们就可以通过这些来实现差异化的处理

 height: calc(100vh - 88rpx - var(--window-top) - var(--window-bottom));

6、快速切换跳转页面

小程序里面是没有地址栏的,切换起来非常麻烦的。 pages.json #配置项列表

1656492795428.png

condition

启动模式配置,仅开发期间生效,用于模拟直达页面的场景,如:小程序转发后,用户点击所打开的页面。、

1656757316689.png

"condition": {
		"current": 1, //当前激活的模式(list 的索引项)
		"list": [
			{
				"name": "pages/goods_list/goods_list", //模式名称
				"path": "pages/goods_list/goods_list" //启动页面,必选
			},
			{
				"name": "pages/pay/pay", //模式名称
				"path": "pages/pay/pay" //启动页面,必选
			}
		]
	},

7、文字穿透

1656576040275.png 这是小程序独有的 , 视图容器

功能描述

覆盖在原生组件之上的文本视图。

可覆盖的原生组件包括 mapvideocanvascameralive-playerlive-pusher

只支持嵌套 cover-viewcover-image,可在 cover-view 中使用 button。组件属性的长度单位默认为px,2.4.0起支持传入单位(rpx/px)。

意思就是:普通的盒子是挡不住这个文字的,如果重新文字穿透问题。可以不盒子换成cover-view。

并且 文档中还说了 原生组件说明 原生组件

小程序中的部分组件是由客户端创建的原生组件,这些组件有:

原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。

然后在设置 z-index 就可以遮住了

这是老版本的写法了,新版本直接设置 z-index 就可以了

8、云开发

适用于小项目,大项目需要前后端分离的。

注册一个新的项目

1656578765284.png

1656578878686.png

存储