header 组件
起步
header 组件需完成:
1.头部商家信息的展示
2.完成公告部分
3.弹出层的实现
在这个过程中,很重要的一步是通过异步请求后端数据接口,接收并渲染相关商家数据。
如何实现:使用第三方插件 vue-resource,通过在父组件(App.vue)发送 Ajax 请求获得后端数据,然后通过 header 的 Prop 属性将数据传递给 header 组件。
知识准备
vue-resource 简介
1)vue-resource 定位
vue-resource 是 Vue.js 的一款插件,它可以通过 XMLHttpRequest 或 JSONP 发起请求并处理响应。也就是说,它可以用于处理前后端数据请求、数据交互,达到与 $.ajax 同等功能。
2)vue-resource 特点:
2-1)支持 Promise API 和 URI Templates
Promise是ES6的特性,Promise的中文含义为“先知”,Promise对象用于异步计算。URI Templates表示URI模板,有些类似于ASP.NET MVC的路由模板。
2-2)支持拦截器(https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors)
拦截器是全局的,拦截器可以在请求发送前和发送请求后做一些处理。拦截器在一些场景下会非常有用,比如请求发送前在headers中设置access_token,或者在请求失败时,提供共通的处理方式。
2-3)支持主流的浏览器:和Vue.js一样,vue-resource除了不支持IE 9以下的浏览器,其他主流的浏览器都支持。
2-4)支持 vue 1.0版本 及 vue 2.0版本
2-5)体积小:vue-resource非常小巧,在压缩以后只有大约14KB,服务端启用gzip压缩后只有5.3KB大小,这远比jQuery的体积要小得多。
PS:
1.支持 promise api 的形式,就是通过同步链式的写法来支持异步的操作。
2.关于 vue-resource 更多知识点:http://www.cnblogs.com/keepfool/p/5657065.html
1.什么是组件?
组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。
所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。
2.使用组件
2)组件组合
组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。然而,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。
在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。看看它们是怎么工作的。
3.Prop
组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。
子组件要显式地用 props 选项声明它预期的数据:
Vue.component('child', {
// 声明 props
props: ['message'],
// 就像 data 一样,prop 也可以在模板中使用
// 同样也可以在 vm 实例中通过 this.message 来使用
template: '<span>{{ message }}</span>'
})
然后我们可以这样向它传入一个普通字符串:
<child message="hello!"></child>
结果:
hello!
2)动态 Prop
与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:
<div> <input v-model="parentMsg"> <br> <child v-bind:my-message="parentMsg"></child> </div> <!-- v-bind 的缩写语法 --> <!-- <child :my-message="parentMsg"></child> -->
如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:
todo: {
text: 'Learn Vue',
isComplete: false
}
然后:
<todo-item v-bind="todo"></todo-item>
将等价于:
<todo-item v-bind:text="todo.text" v-bind:is-complete="todo.isComplete" ></todo-item>
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created 钩子可以用来在一个实例被创建之后执行代码:
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
// => "a is: 1"
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 destroyed。生命周期钩子的 this 上下文指向调用它的 Vue 实例。
开发
header组件内容
如下图,包括外层组件以及弹出层。在对这两块进行详细设置之前,需要先通过异步请求后端数据接口,接收并渲染相关商家数据。
所以本节有以下三部分知识点:
1.获取数据
2.外层组件
3.弹出层
PS:本节所有代码在文章底部。
一、获取数据
获取后台商家数据,获得 seller对象数据
1.安装第三方插件 vue-resource,在 main.js 中引用并且注册该插件。
安装方法:npm install vue-resource --save
// 引用
import Vueresource from 'vue-resource';
// 全局注册
Vue.use(Vueresource);
2.在 App.vue 中定义一个 seller 对象:使用 data() 方法,在函数内返回一个空对象 seller。
Q:为什么是用 data() 方法而不是 data() 对象?
A:因为在 vue.js 中,组件是可以被复用的,也就是说,如果使用对象而不是函数的话,某一组件的修改会影响另外一个组件,所以这里定义的是方法而非函数。更多知识可看官网:data
必须是函数 。
3.在 APP.vue 中发送 ajax 请求获取后台商家数据(seller 对象),然后塞给上一步骤的空对象就可以了。
1)什么时候去发送这样一个 ajax 请求?vue实例化的时候都有一个生命周期,其中有个钩子叫 created() ,vue实例被生成后就调用这个函数。一般可以在created函数中调用ajax获取页面初始化所需的数据。一个vue实例被生成后还要绑定到某个html元素上,之后还要进行编译,然后再插入到document中。每一个阶段都会有一个钩子函数,方便开发者在不同阶段处理不同逻辑。
2)在 created 里面通过 get() 方法去获取数据,获取地址是“ /api/seller ”,请求发送以后,调用 then() 方法。then() 方法中的第一个参数是成功的回调,第二个参数是失败的回调。因为这里是模拟后台数据,所以一定是成功的,所以只写成功的回调方法。response 是一个方法,返回的内容不是json对象,而是属性。看 文档 可以知道,body参数返回的数据类型是 object 类型,正是我们所需要的。
3)接着判断 errno 是否为 0,为 0 表示正常,然后返回数据。关于 errno 的设置在 webpack.dev.conf.js 中可以找到。这里定义一个常量 ERR_OK 为 0,是为了阅读代码时更加清晰,知道 0 代表什么,无需通篇查找 errno = 0 的含义,代码更为友好。
1 const ERR_OK = 0; 2 // 注册 3 export default { 4 created() { 5 this.$http.get('/api/seller').then(response => { 6 // get body data 7 response = response.body; 8 console.log(response); 9 if (response.errno === ERR_OK) { 10 this.seller = response.data; 11 console.log(response.data); 12 } 13 }); 14 } 15 ... 16 };
PS:在 network 可以看到有 seller 的数据,在 console 可以看到返回的数据是 object 类型,同时也可以看到这些数据加上了很多 get 和 set 方法。这是 vue 自动给这些字段添加的方法,用于监听这些字段的行为。如果对象某些值被修改时,这些变化就能被监听到,并且及时映射到 DOM 上。关于 get 和 set 方法,在导语1——vue简单介绍的数据响应原理知识点处有提及到。
接下来进行外层组件和弹出层的设计。这两部分都需要动态获取数据,需要将 seller 对象传递给 header 组件,让 header 组件将其渲染出来。可以通过 v-bind: 将其进行数据绑定,v-bind: 缩写为 : 。
先看一下代码框架:
content-wrapper:外层组件中的上层部分
bulletin-wrapper:外层组件中的公告栏部分
background:header 组件的背景层
detail:弹出层
二、外层组件
下图可以看出,外层组件大体分为上下两个部分:上层又可分为左右两部分,左边是图片,右边是商家相关信息;下层是公告栏相关信息。除此之外,还有一个背景层。
1.上层部分
1 <div class="content-wrapper"> 2 <div class="avatar"> 3 <img :src="seller.avatar" alt="" width="64" height="64"> 4 </div> 5 <div class="content"> 6 <div class="title"> 7 <span class="brand"></span> 8 <span class="name">{{seller.name}}</span> 9 </div> 10 <div class="description"> 11 {{seller.description}}/{{seller.deliveryTime}}分钟送达 12 </div> 13 <div class="support" v-if="seller.supports"> 14 <span class="icon" :class="classMap[seller.supports[0].type]"></span> 15 <span class="text">{{seller.supports[0].description}}</span> 16 </div> 17 </div> 18 <div v-if="seller.supports" class="support-count" v-on:click="showDetail"> 19 <span class="count">{{seller.supports.length}}个</span> 20 <i class="icon-keyboard_arrow_right"></i> 21 </div> 22 </div>
1)Q:引用头像图片地址,是 :src 而不是 src的原因
A:selller.avatar 一开始是不存在的,因为 seller 是空对象,其数据是通过 ajax 请求异步获取数据然后填充得来的。所以如果直接用 src ,编译就会直接解析 src,此时的 seller 还是是空的,自然就找不到 selller.avatar,就会报错。
2)Q:为什么要用 v-if 判断存不存在 supports ?
A:因为这是由数据决定的,有可能该数组不存在。
3)Q:不加 v-if 会怎么样?
A:在这里的话,不加 v-if 也能正确显示信息。但是控制台会报错。这是因为在 App.vue 的 html 代码中,我们将 seller 对象传递给了 header 组件,而在下边 JavaScript 代码中, seller 对象一开始被设置为空对象,其数据是通过 ajax 请求异步获取数据然后填充得来的。所以此时 header 组件接收的只是一个空的 seller 对象,本来就是 undefined 。编译器此时解析的话,自然找不到 seller 对象的相关数据,自然就报错了。
4)support 设置
如图所示,这一块的设置需要根据后端返回的数据,进行判断后动态的显示。因为有可能今天是"在线支付满28减5"排第一位,明天是"单人精彩套餐"排第一位,程序需要根据数据准确显示出第一位的内容。
问题:data.json 的 supports 数组只有type 和 description 属性,怎么才能正确取得图片呢?
设计思路:每一个 type 对应到前端中不同的 class,然后给不同的 class 添加不同的图片就可以了。所以可以给 .support 动态绑定一个class,即根据后端返回的 type(第一位),确定到底绑定哪一个 class(例如减对应的 class 为 decrease),然后将该 class 传递给动态绑定的class(:class),这样就能正确显示。
如何实现:
第一步:在组件 created() 中,定义一个 classMap,将 type 与 class 进行绑定(在 data.json 文件中,supports 数组中 type 顺序:减,折,套餐,发票,保障,则对应class名称:decrease,discount,special,invoice,guarantee);
第二步:给不同的 class 设置不同的图片;
第三步:将数组 classMap 绑定到 class 上,即 :class="classMap[seller.supports[0].type]。
如下图,html 中的 classMap 可以访问到this.classMap,进而访问到数组里的 class,每一个 class 又有真正对应的值。这样就能正确显示出图片。
检验成果:可以在data.json中将type=2的那一块数据调到顶端,重新编译刷新可以发现网页的数据发生了变化。
5)图标字体的使用:<i class="icon-keyboard_arrow_right"></i>
在 icon.styl 文件中可以找到类名。
6)浮层效果的入口:绝对定位,且定位在右下角,这个定位是相对于content-wrapper
7)CSS设置
7-1)在 base.styl 文件中,定义通用样式。
7-2)左右两块区域是并排,可以直接设置为 inline-block。
7-3)左右两块区域中间有空格,这是因为空白字符(换行符、空格符等)的影响,为消除该影响,可以设置 font-size 为 0。
7-4)根据不同 dpr 进行不同图片的显示,在 mixin.styl 中设置该函数。
1 bg-image($url) 2 // 正常情况下 3 background-image url($url + "@2x.png") 4 // 最小 dpr=3 的情况下 5 @madia (-webkit-min-device-pixel-ratio 3) (-min-device-pixel-ratio 3) 6 background-image url($url + "@3x.png")
7-5)不能像加载 js 库一样,进行省略别名的使用。因为那是针对 import js 库,css 这边是不可以的,所以这里只能写相对路径。
@import '../../common/stylus/mixin'
7-6)Chrome 浏览器默认最小字体是 12px,所以设置为 10px 没办法生效,可以在手机端看效果。
8)图片地址
在控制台可以看到,图片在编译过程被webpack自动转换成 base64 编码格式,然后导入代码了,所以这里是 base64 的地址。
2.公告栏部分
公告栏拆分:左边图片,中间文字,右边图标字体(箭头),点击箭头可以展开浮层。
1 <div class="bulletin-wrapper" v-on:click="showDetail"> 2 <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span> 3 <i class="icon-keyboard_arrow_right"></i> 4 </div>
1)清除空白字符的影响
方法1:设置 font-size 为 0 。
方法2:将两个 span 写在一起,不要换行。
这里选第二个方法,因为后边要进行省略号的设置,如果选第一个方法,会看不见省略号。
2)省略号设置
1 /*要为文字限定显示范围,位置应该限定到箭头前面*/ 2 padding 0 22px 0 12px 3 /*文字超过宽度省略的设置*/ 4 white-space nowrap 5 overflow hidden 6 text-overflow ellipsis
要进行省略号设置,必须先限定文字显示范围,所以这里设置 padding 时,是设置到省略号前面,代表从省略号到最后那段距离都不能显示文字。
后面三行代码是为了设置省略号:white-space nowrap(不自动换行)、overflow hidden(超过范围隐藏)、text-overflow ellipsis(省略号设置)。
3)图标字体(箭头),即浮层效果的入口:绝对定位,这个定位是相对于父元素 bulletin-wrapper 。
4)点击效果的实现
给元素添加点击事件 v-on:click="showDetail" ,在 method() 方法里定义点击事件。
detailShow 是弹出层设置的用于控制弹出层显示隐藏的一个变量,detailShow 值为 true 时显示,为 false 时隐藏。更多知识在弹出层进行讲解。
5)CSS设置
左边图片和中间文字不是居中对齐,这是因为两部分的高度不同,所以设置 vertical-align top,让两部分顶部对齐。此时发现左边图片在上方,可以设置文字的 margin-top:(28-12)/2=8px (父元素高28px,文字12px),即可居中对齐。
3.背景层
可以看到,背景是虚化的图片。
1)给背景层设置绝对定位,这个定位是相对于父元素 header 。
2)设置 z-index 为 -1,让该背景层处于最下方,不要遮盖掉其他设置。
3)宽高撑满整个背景层。
4)使用 filter 滤镜产生模糊效果
设置完在浏览器查看时,会发现背景渗透到公告下,这是因为设置了blur。可以在父元素 header 上添加 overflow:hidden ,即可解决该问题。
三、弹出层
浮层高度大于手机时,应该有下拉功能。不管浮层中内容多少,关闭按钮都必须固定位于下方,这里将用到 css sticky fotter 布局来实现该功能。该布局一般会有 detail-wrapper 和 detail-close(关闭按钮) 两个层,而真正内容写在 detail-main 中:
1 <div class="detail-wrapper clearfix"> 2 <div class="detail-main"></div> 3 </div> 4 <div class="detail-close"></div>
下面将首先对弹出层的代码框架布局进行详细讲解,然后再把弹出层分成弹出层内容还有关闭按钮两部分进行讲解。
1.代码框架布局
以下是弹出层的代码框架:
1)显示隐藏设置(v-show)
弹出层是应当实现显示和隐藏功能,这里可以使用 vue 的条件渲染 v-show 来实现。设置一个 detailShow 变量,通过改变这个变量的值(true/false),来控制弹出层显示或隐藏。
如何实现:给vue实例中添加一个选项data(),这个 data() 是一个function(这是之前说过的组件复用问题),该方法会返回一个 object,object 里面的变量就是它所需要去跟踪依赖的一些变量。也就是说,vue在实例化时,会对 data 对象中的变量去遍历,然后添加 getter 和 setter 方法,这样,当变量变化时,DOM 会根据变量的变化而变化,所以这里用v-show控制detailShow。
使用 v-show 的好处:不需要额外写 DOM 代码,都是直接操作变量即可。
PS:代码中有两处需要涉及到 detailShow 的设置,为这两个层添加 click 事件。
原理:在方法里面改变 detailShow 的值,因为 detailShow 是依赖跟踪的,当点击之后调用该方法,this.detailShow 会依赖跟踪到 data 里面的 detailShow,并改变其值,因为是依赖跟踪,vue 可以检测到其变化,并将该变化映射到 DOM 上,这样就可以通过改变数据的值来实现弹层的悬浮或者隐藏。
2)过渡渐变(transition)
弹出层弹出的时候,背景颜色应该由浅到深,弹出层隐藏的时候,背景颜色应该由深到浅。这种渐变的设置可以用 vue 的内置组件 transition 实现。以下是基本的 HTML 结构:
transition 有一个 name 属性,添加该属性后,会自动生成 CSS 过渡类名。例如:name: 'fade' 将自动拓展为.fade-enter,.fade-enter-active等。在这里设置背景颜色后,再设置过渡的类名 .fade-enter,.fade-enter-active,.fade-leave-to,.fade-leave-active 的样式即可。
可在浏览器中查看效果:
3)CSS设置
3-1)弹出层相对于窗口应该是 fixed 结构。
3-2)弹出层应该位于所有层的最上方,所以父元素设置 z-index 为 100 。
3-3)当内容超过屏幕高度,弹出层应该产生滚动条,所以设置 overflow 属性。
3-4)背景文字虚化设置 backdrop-filter: blur(10px),这个设置支持 ios 设备,可以在 ios 手机上查看效果。可以查看 backdrop-filter 的使用范围。
2.弹出层内容
1)CSS设置
1-1)要给 .detail-wrapper 层加上清除浮动 clearfix ,样式设置写在 base.styl 。
1-2).detail-wrapper 的最小高度应该跟视口高度一样,所以设置为 100%。
1-3).detail-main 中设置 padding-bottom 64px。如果没有 padding-bottom,.detail-main 的内容会盖住关闭按钮。加了后会撑开高度,这部分高度会给按钮留住位置。
2)星级组件设置(star.vue)
星级评价这一块内容在之后多个页面会使用到,所以将其抽象成 star 组件。
在这里设置为总共五颗星,评分范围为 0-5,评分规则是向下取0.5倍数的值,比如4到4.4会变成4(显示四颗全星,一颗无星),4.5到4.9会变成4.5(显示四颗全星和一颗半星)。此处有一个思路,就是根据不同分数范围制作不同的图片,然后引用图片,如下图。
但是为了增加代码的可用性,最好不要这样做,我们可以通过分数来计算到底有多少颗全星和无星,以及是否有半星。
先创建相应的文件,在 star.vue 中编写代码。
实现代码如下:
2-1)HTML 代码
1 <template> 2 <div class="star" :class="starType"> 3 <span v-for="itemClass in itemClasses" :class="itemClass" class="star-item"></span> 4 </div> 5 </template>
先给星级组件添加父元素,类名是 star,将星级组件包裹起来,并 v-bind 方式定义一个 starType 。再添加评星,用 v-for 指令遍历 itemClasses 数组,这个数组是最后所有星星存放的数组,遍历该数组,得到不同的 itemClass,再通过 CSS 设置不同 itemClass ,最终就可以得到不同的星级评定。
2-2)JavaScript 代码
1 <script type="text/ecmascript-6"> 2 const LENGTH = 5; 3 const CLS_ON = 'on'; 4 const CLS_HALF = 'half'; 5 const CLS_OFF = 'off'; 6 7 export default { 8 props: { 9 size: { 10 type: Number 11 }, 12 score: { // 评价的分数 13 type: Number 14 } 15 }, 16 computed: { 17 starType() { 18 return 'star-' + this.size; 19 }, 20 itemClasses() { 21 let result = []; // 最终所有星星存放的数组 22 let score = Math.floor(this.score * 2) / 2; 23 let hasDecimal = score % 1 !== 0; // 小数 24 let integer = Math.floor(score); // 整数 25 for (let i = 0; i < integer; i++) { // 全星 26 result.push(CLS_ON); 27 } 28 if (hasDecimal) { // 是否有半星(半星最多有一个) 29 result.push(CLS_HALF); 30 } 31 while (result.length < LENGTH) { // 无星 32 result.push(CLS_OFF); 33 } 34 return result; 35 } 36 } 37 }; 38 </script>
以下是部分 CSS 代码:
1 .star 2 ... 3 &.star-48 4 .star-item 5 ... 6 &.on 7 bg-image('star48_on') 8 &.half 9 bg-image('star48_half') 10 &.off 11 bg-image('star48_off') 12 &.star-36 13 ... 14 &.star-24 15 ...
2-2-1)先通过 props 去接收外部传来的两个参数:size 和 score,这两个参数都是 Number 类型。size 的值是 24,36,48 中的一个,是图片的尺寸类型,score是评分。
2-2-2)starType 是通过 size 参数计算来的,可以通过 conputed 计算属性计算出来,因为参数 size 本身会有 getter 和 setter 方法依赖跟踪。通过计算属性拼接出starType 的值,从而产生不同的 starType,即不同的 class,接下来对 class(star-48,star-36,star-24)进行样式设计即可。其中,on、half、off 分别代表全星、半星、无星。
2-2-3)itemClasses 是数组,要根据 score 进行计算,数组内容其实就是on、half,off 的组合,通过这些类组成星级评定。比如三颗半星,那么 itemClasses 应该是有三个 on,一个 half,一个 off。依旧在 conputed 计算属性里写代码,因为 itemClasses 是数组,所以先定义一个 result 数组,然后进行 score 的计算转化。在转换后的 score 的基础上,对其求余,有余数代表有一颗半星(half),对 score 进行取整,该整数就是全星(on)的个数。注意:半星最多只有一个。最后就是将 on 和 half 写进 result 数组,判断 result 长度是否为 5,不为 5 的话,要补上 off。
PS:可以看到一开始设置了四个常量 LENGTH、CLS_ON、CLS_HALF、CLS_OFF,这是为了增加代码的可用性。
2-2-4)在 header.vue 中通过 components 注册 star 组件并引用。
<star :size="48" :score="seller.score"></star>
components: {
star
}
3)小标题的设置
小标题的设置,从样式上看,应该是左右两边的线有一个自适应的能力,可根据屏幕大小延伸,标题居中,标题与线之间还有一部分空白。此处用到了一个经典布局,即 flex 布局 。下面是 HTML代码,从代码中可以看到,这里用了三个 div 元素而不是 span 元素,这是因为用 span 的话,在一些 Android 浏览器上会产生一些间距问题,因此这里用 div 。
1 <div class="title"> 2 <div class="line"></div> 3 <div class="text">优惠信息</div> 4 <div class="line"></div> 5 </div>
以下是部分 CSS 代码,从代码中可以看到我们只写了一句 display flex,并没有写其它兼容性代码。这是因为编译时,vue-loader 中有 postcss 工具,可以自动添加有兼容性代码样式。postcss 是根据 can i use 官网写的代码。
1 .title 2 display flex 3 width 80% 4 margin 28px auto 24px auto 5 .line 6 flex 1 7 position relative 8 top -6px 9 border-bottom 1px solid rgba(255, 255, 255, 0.2) 10 .text 11 padding 0 12px 12 font-weight 700 13 font-size 14px
PS:关于 flex 布局(只挑本节涉及到的知识点进行讲解)
1. flex-grow属性:定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
.item { flex-grow: <number>; /* default 0 */ }
如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
2. flex-shrink属性:定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
.item { flex-shrink: <number>; /* default 1 */ }
如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。负值对该属性无效。
3. flex-basis属性:定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
.item { flex-basis: <length> | auto; /* default auto */ }
它可以设为跟width或height属性一样的值(比如350px),则项目将占据固定空间。
4.flex属性:是 flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto。后两个属性可选。
.item { flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] }
该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。
建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。
4)优惠信息的设置
使用无序列表元素 ul 和 li,用 v-for 遍历 data.json 文件中的 supports 数组,然后展示出来。因为 v-for 支持一个可选的第二个参数为当前项的索引,所以我们写大量额外的代码,只需要在 v-for 遍历时,加上 index,下边就可以可以很容易的取到正确的图片和文字了(classMap是五个图片类名的数组)。更多关于 v-for的知识,可以查看:列表渲染 以及 v-for-遍历数组时的参数顺序-变更 。
1 <ul v-if="seller.supports" class="supports"> 2 <li class="support-item" v-for="(item, index) in seller.supports"> 3 <span class="icon" :class="classMap[seller.supports[index].type]"></span> 4 <span class="text">{{seller.supports[index].description}}</span> 5 </li> 6 </ul>
3.关闭按钮
1)CSS设置
margin -64px auto 0 auto:因为 .detail-wrapper 与 .detail-close 同级,而当 .detail-wrapper 满屏时,.detail-close 是不会抢占底部的空间,所以需要设置一个向上的margin,这样才能一直出现在屏幕的最底部。
2)添加关闭按钮功能
在 HTML 代码中加上 click 事件,注意 v-on:click 可以缩写为 @click 。
1 <div class="detail-close" v-on:click="hideDetail"> 2 <i class="icon-close"></i> 3 </div>
在 JavaScript 代码中将 detailShow 的值设置为 false(弹出层隐藏)。
1 methods: { 2 hideDetail() { 3 this.detailShow = false; 4 } 5 },
至此,header组件已全部完成。
附代码:
1 <template> 2 <div id="app"> 3 <v-header :seller="seller"></v-header> 4 <div class="tab border-1px"> 5 <div class="tab-item"> 6 <!-- 使用 router-link 组件来导航. --> 7 <!-- 通过传入 `to` 属性指定链接. --> 8 <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> 9 <router-link to="/goods">商品</router-link> 10 </div> 11 <div class="tab-item"> 12 <router-link to="/ratings">评论</router-link> 13 </div> 14 <div class="tab-item"> 15 <router-link to="/seller">商家</router-link> 16 </div> 17 </div> 18 <!--内容区--> 19 <div class="content"> 20 <!-- 路由出口 --> 21 <!-- 路由匹配到的组件将渲染在这里 --> 22 <router-view></router-view> 23 </div> 24 </div> 25 26 27 </template> 28 29 <script type="text/ecmascript-6"> 30 // 引用 31 import header from 'components/header/header.vue'; 32 33 const ERR_OK = 0; 34 // 注册 35 export default { 36 data() { 37 return { 38 seller: {} 39 }; 40 }, 41 created() { 42 this.$http.get('/api/seller').then(response => { 43 // get body data 44 response = response.body; 45 console.log(response); 46 if (response.errno === ERR_OK) { 47 this.seller = response.data; 48 console.log(response.data); 49 } 50 }); 51 }, 52 components: { 53 'v-header': header 54 } 55 }; 56 </script> 57 58 <style lang="stylus" rel="stylesheet/stylus"> 59 @import "common/stylus/mixin.styl" 60 #app 61 .tab 62 display flex 63 width 100% 64 height 40px 65 line-height 40px 66 border-1px(rgba(7, 17, 27, 0.1)) 67 .tab-item 68 flex 1 69 text-align center 70 /* &:表示父元素,即.tab-item */ 71 & > a 72 /* 区块点击才能有反应 */ 73 display block 74 font-size 14px 75 color rgb(77, 85, 93) 76 &.active 77 color rgb(240, 20, 20) 78 </style>
1 <template> 2 <div class="header"> 3 <div class="content-wrapper"> 4 <div class="avatar"> 5 <img :src="seller.avatar" alt="" width="64" height="64"> 6 </div> 7 <div class="content"> 8 <div class="title"> 9 <span class="brand"></span> 10 <span class="name">{{seller.name}}</span> 11 </div> 12 <div class="description"> 13 {{seller.description}}/{{seller.deliveryTime}}分钟送达 14 </div> 15 <div class="support" v-if="seller.supports"> 16 <span class="icon" :class="classMap[seller.supports[0].type]"></span> 17 <span class="text">{{seller.supports[0].description}}</span> 18 </div> 19 </div> 20 <div v-if="seller.supports" class="support-count" v-on:click="showDetail"> 21 <span class="count">{{seller.supports.length}}个</span> 22 <i class="icon-keyboard_arrow_right"></i> 23 </div> 24 </div> 25 <div class="bulletin-wrapper" v-on:click="showDetail"> 26 <span class="bulletin-title"></span><span class="bulletin-text">{{seller.bulletin}}</span> 27 <i class="icon-keyboard_arrow_right"></i> 28 </div> 29 <div class="background"> 30 <img :src="seller.avatar" width="100%" height="100%"> 31 </div> 32 <transition name="fade"> 33 <div v-show="detailShow" class="detail"> 34 <div class="detail-wrapper clearfix"> 35 <div class="detail-main"> 36 <h1 class="name">{{seller.name}}</h1> 37 <div class="star-wrapper"> 38 <star :size="48" :score="seller.score"></star> 39 </div> 40 <div class="title"> 41 <div class="line"></div> 42 <div class="text">优惠信息</div> 43 <div class="line"></div> 44 </div> 45 <ul v-if="seller.supports" class="supports"> 46 <li class="support-item" v-for="(item, index) in seller.supports"> 47 <span class="icon" :class="classMap[seller.supports[index].type]"></span> 48 <span class="text">{{seller.supports[index].description}}</span> 49 </li> 50 </ul> 51 <div class="title"> 52 <div class="line"></div> 53 <div class="text">商家公告</div> 54 <div class="line"></div> 55 </div> 56 <div class="bulletin"> 57 <p class="content">{{seller.bulletin}}</p> 58 </div> 59 </div> 60 </div> 61 <div class="detail-close" v-on:click="hideDetail"> 62 <i class="icon-close"></i> 63 </div> 64 </div> 65 </transition> 66 </div> 67 68 </template> 69 70 <script type="text/ecmascript-6"> 71 import star from '../star/star.vue'; 72 73 export default { 74 props: { 75 seller: { 76 type: Object 77 } 78 }, 79 // vue的data必须用function,因为可能有多个组件实例,组件又可以复用,为了相互之间不要互相影响,必须设置data为function 80 // 之前有过关于getter和setter知识点的讲解 81 // vue在new一个是实例是,解析到data函数,会给其中的参数加上getter和setter方法去监听它的变化 82 data() { 83 return { 84 detailShow: false 85 }; 86 }, 87 methods: { 88 showDetail() { 89 this.detailShow = true; 90 }, 91 hideDetail() { 92 this.detailShow = false; 93 } 94 }, 95 created() { 96 this.classMap = ['decrease', 'discount', 'special', 'invoice', 'guarantee']; 97 }, 98 components: { 99 star 100 } 101 }; 102 </script> 103 104 <style lang="stylus" rel="stylesheet/stylus"> 105 @import '../../common/stylus/mixin' 106 107 .header 108 position relative 109 /*overflow hidden*/ 110 color #fff 111 background rgba(7, 17, 27, 0.5) 112 .content-wrapper 113 position relative 114 padding 24px 12px 18px 24px 115 /* 消除空白字符的影响 */ 116 font-size 0 /*background-color rgba(7, 17, 27, 0.5) 117 blur 5px 118 */ 119 .avatar 120 /* 左右两个区域并排显示 */ 121 display inline-block 122 vertical-align top 123 img 124 border-radius 2px 125 .content 126 display inline-block 127 margin-left 16px 128 .title 129 margin 2px 0 8px 0 130 .brand 131 /* 因为是 span ,必须设置为块级元素才能指定宽高 */ 132 display inline-block 133 /* 因为两个inline-block是不一样搞得,需要设置对齐方式 */ 134 vertical-align top 135 width 30px 136 height 18px 137 bg-image('brand') 138 background-size 30px 18px 139 background-repeat no-repeat 140 .name 141 margin-left 6px 142 font-size 16px 143 line-height 18px 144 font-weight bold 145 .description 146 margin-bottom 10px 147 line-height 12px 148 font-size 12px 149 .support 150 margin 10px 0 2px 0 151 .icon 152 display inline-block 153 vertical-align top 154 width 12px 155 height 12px 156 margin-right 4px 157 background-size 12px 12px 158 background-repeat no-repeat 159 &.decrease 160 bg-image('decrease_1') 161 &.discount 162 bg-image('discount_1') 163 &.guarantee 164 bg-image('guarantee_1') 165 &.invoice 166 bg-image('invoice_1') 167 &.special 168 bg-image('special_1') 169 .text 170 line-height 12px 171 font-size 10px 172 .support-count 173 position absolute 174 right 12px 175 bottom 14px 176 padding 0 8px 177 height 24px 178 line-height 24px 179 border-radius 14px 180 background rgba(0, 0, 0, 0.2) 181 text-align center 182 .count 183 /*字体和图标字体是不一样的大小,所以设置vertical-align top进行顶部对齐*/ 184 vertical-align top 185 font-size 10px 186 .icon-keyboard_arrow_right 187 margin-left 2px 188 /*因为在base.styl中设置了line-height:1,所以这里要设置line-height,否则无法垂直居中*/ 189 line-height 24px 190 font-size 10px 191 .bulletin-wrapper 192 position relative 193 height 28px 194 line-height 28px 195 /*要为文字限定显示范围,位置应该限定到箭头前面*/ 196 padding 0 22px 0 12px 197 /*文字超过宽度省略的设置*/ 198 white-space nowrap 199 overflow hidden 200 text-overflow ellipsis 201 background rgba(7, 17, 27, 0.2) 202 .bulletin-title 203 display inline-block 204 vertical-align top 205 margin-top 8px 206 width 22px 207 height 12px 208 bg-image('bulletin') 209 background-size 22px 12px 210 background-repeat no-repeat 211 .bulletin-text 212 vertical-align top 213 margin 0 4px 214 text-align center 215 font-size 10px 216 .icon-keyboard_arrow_right 217 position absolute 218 /*因为在base.styl中设置了line-height:1,所以这里要设置line-height,否则无法垂直居中*/ 219 font-size 10px 220 right 12px 221 bottom 8px 222 .background 223 position absolute 224 top 0 225 left 0 226 width 100% 227 height 100% 228 z-index -1 229 /*使用filter滤镜产生模糊效果*/ 230 filter blur(10px) 231 .detail 232 position fixed 233 z-index 100 234 top: 0 235 left 0 236 width 100% 237 height 100% 238 /*当内容超过屏幕高度,可以产生滚动条*/ 239 overflow auto 240 background-color: rgba(7, 17, 27, 0.8) 241 backdrop-filter: blur(10px) 242 /*从enter到transition,最终显示效果为transition*/ 243 &.fade-enter-active, &.fade-leave-active 244 transition opacity 0.5s 245 &.fade-enter, &.fade-leave-to 246 opacity 0 247 .detail-wrapper 248 /*要定义宽度,撑开空间,下面一些居中设置才能实现*/ 249 width 100% 250 /*铺满整个屏幕*/ 251 min-height 100% 252 .detail-main 253 margin-top 64px 254 /*下边距包括两部分:一部分是边距,一部风是关闭按钮的高度*/ 255 padding-bottom 64px 256 .name 257 line-height 16px 258 text-align center 259 font-size 16px 260 font-weight 700 261 .star-wrapper 262 margin-top 18px 263 padding 2px 0 264 text-align center 265 .title 266 display flex 267 width 80% 268 margin 28px auto 24px auto 269 .line 270 flex 1 271 position relative 272 top -6px 273 border-bottom 1px solid rgba(255, 255, 255, 0.2) 274 .text 275 padding 0 12px 276 font-weight 700 277 font-size 14px 278 .supports 279 width 80% 280 margin 0 auto 281 .support-item 282 padding 0 12px 283 margin-bottom 12px 284 font-size 0 285 &:last-child 286 margin-bottom 0 287 .icon 288 display inline-block 289 vertical-align top 290 width 16px 291 height 16px 292 margin-right 6px 293 background-size 16px 16px 294 background-repeat no-repeat 295 &.decrease 296 bg-image('decrease_2') 297 &.discount 298 bg-image('discount_2') 299 &.guarantee 300 bg-image('guarantee_2') 301 &.invoice 302 bg-image('invoice_2') 303 &.special 304 bg-image('special_2') 305 .text 306 /*与图片高度一样,才能垂直居中*/ 307 line-height 16px 308 font-size 12px 309 .bulletin 310 width 80% 311 margin 0 auto 312 .content 313 padding 0 12px 314 line-height 24px 315 font-size 12px 316 .detail-close 317 position relative 318 width 32px 319 height 32px 320 margin -64px auto 0 auto 321 clear both 322 font-size 32px 323 </style>
1 <template> 2 <div class="star" :class="starType"> 3 <span v-for="itemClass in itemClasses" :class="itemClass" class="star-item"></span> 4 </div> 5 </template> 6 7 <script type="text/ecmascript-6"> 8 const LENGTH = 5; 9 const CLS_ON = 'on'; 10 const CLS_HALF = 'half'; 11 const CLS_OFF = 'off'; 12 13 export default { 14 props: { 15 size: { 16 type: Number 17 }, 18 score: { // 评价的分数 19 type: Number 20 } 21 }, 22 computed: { 23 starType() { 24 return 'star-' + this.size; 25 }, 26 itemClasses() { 27 let result = []; // 最终所有星星存放的数组 28 let score = Math.floor(this.score * 2) / 2; 29 let hasDecimal = score % 1 !== 0; // 小数 30 let integer = Math.floor(score); // 整数 31 for (let i = 0; i < integer; i++) { // 全星 32 result.push(CLS_ON); 33 } 34 if (hasDecimal) { // 是否有半星(半星最多有一个) 35 result.push(CLS_HALF); 36 } 37 while (result.length < LENGTH) { // 无星 38 result.push(CLS_OFF); 39 } 40 return result; 41 } 42 } 43 }; 44 </script> 45 46 <style lang="stylus" rel="stylesheet/stylus"> 47 @import '../../common/stylus/mixin.styl'; 48 49 .star 50 /*清除空白字符影响*/ 51 font-size 0 52 .star-item 53 /*星星是横向排列*/ 54 display inline-block 55 background-repeat no-repeat 56 &.star-48 57 .star-item 58 width: 20px 59 height: 20px 60 margin-right: 22px 61 background-size: 20px 20px 62 &:last-child 63 margin-right: 0 64 &.on 65 bg-image('star48_on') 66 &.half 67 bg-image('star48_half') 68 &.off 69 bg-image('star48_off') 70 &.star-36 71 .star-item 72 width: 15px 73 height: 15px 74 margin-right: 6px 75 background-size: 15px 15px 76 &:last-child 77 margin-right: 0 78 &.on 79 bg-image('star36_on') 80 &.half 81 bg-image('star36_half') 82 &.off 83 bg-image('star36_off') 84 &.star-24 85 .star-item 86 width: 10px 87 height: 10px 88 margin-right: 3px 89 background-size: 10px 10px 90 &:last-child 91 margin-right: 0 92 &.on 93 bg-image('star24_on') 94 &.half 95 bg-image('star24_half') 96 &.off 97 bg-image('star24_off') 98 </style>
1 body, html 2 margin 0 3 padding 0 4 line-height 1 5 font-weight 200 6 // 字体查找顺序,从前到后,如果所有设置字体都不认识,则返回系统字体 7 font-family 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', Arial, sans-serif 8 9 ul 10 list-style none 11 padding-left 0 12 margin 0 13 14 li 15 list-style none 16 17 h1 18 margin 0 19 20 .clearfix 21 display inline-block 22 &:after 23 display block 24 content "." 25 height 0 26 line-height 0 27 visibility hidden 28 clear both 29 30 //为什么要写 -webkit :因为这里没有兼容性文件控制,所以要手写兼容性代码 31 //本例需要的是1像素的边框,所以当dpr为1.5时,取0.7倍(1.5*0.7=1.05约等于1)。dpr为2时,取0.5倍(2*0.5=1) 32 @madia (-webkit-min-device-pixel-ratio 1.5) 33 (-min-device-pixel-ratio 1.5) 34 .border-1px 35 &::after 36 -webkit-transform scaleY(0.7) 37 transform scaleY(0.7) 38 39 @madia (-webkit-min-device-pixel-ratio 2) 40 (-min-device-pixel-ratio 2) 41 .border-1px 42 &::after 43 -webkit-transform scaleY(0.5) 44 transform scaleY(0.5)
1 border-1px($color) 2 position relative 3 // 进行伪类设置,一定要设置宽度,因为元素脱离了文档流 4 &:after 5 display block 6 position absolute 7 left 0 8 bottom 0 9 width 100% 10 border-top 1px solid $color 11 content ' ' 12 13 border-none() 14 &:after 15 display none 16 17 bg-image($url) 18 // 正常情况下 19 background-image url($url + "@2x.png") 20 // 最小 dpr=3 的情况下 21 @madia (-webkit-min-device-pixel-ratio 3) (-min-device-pixel-ratio 3) 22 background-image url($url + "@3x.png")