层级划分
- 内容层 z-index 100-200
- 导航层 z-index 200-300
- 蒙版层 z-index 300-400
页面蒙版
在开发中,还有一类比较常见,就是我们用的页面蒙版。在做一些操作时,为了避免干扰会用蒙版把没用的内容都遮住。根据固定横栏的经验,全屏覆盖想起来就很简单了,把height 也换成 100% 不就可以了。但是这里我们要注意,这种功能靠百分比做的全屏蒙版不太灵活。假如需要蒙版把标题栏的部分留出来,这时候靠百分比就不灵了。所以在做蒙版的时候通常使用下面这种方式:
<!-- 页面蒙版 -->
<div class="z-mask"></div>
/* 页面蒙版 */
.z-mask{
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,.5);
z-index:300
}
这其中给每个方向上定位的值都设置为 0,就会把蒙版撑开到全屏。这样做的好处是假如需要留出来标题栏,就可以调整 top 值来实现,需要留下面导航栏就调整 bottom 值来实现,非常方便。
内容区
/* 内容区 */
.z-content{
height: 100%;
box-sizing: border-box;
overflow-y: auto;
}
/* 根据header和navbar自动适应内容区高度 */
.z-header ~ .z-content{
padding-top: 45px;
}
.z-navbar ~ .z-content{
padding-bottom: 50px;
}
移动端1px实现
使用svg画线,svg的1px是真的对应1px
- postcss-write-svg 插件处理移动端1px
npm i postcss-write-svg -S
.postcssrc.js
module.exports = {
"plugins": {
"postcss-write-svg": {
utf8: false
},
}
}
@svg border-1px{
height:4px;
width:4px;
@rect{
fill:transparent;
width:100%;
height:100%;
stroke-width: 25%; // 边框宽度 4px * 25%(即1px)
stroke: var(--color, black); // 颜色
}
}
.test{
margin:100px;
width:100px;
height:100px;
border:1px solid;
border-image:@svg(border-1px param(--color:#f50)) 1 stretch;
}
组件设计
button组件
button功能
- 类型(默认样式 ,自定义样式)
- 尺寸 (大,默认,小)
- 图标 (左,右)
- 加载状态
- 块级元素
- 禁用
button样式的全局变量
//button component
//type:default
@button-height:32px;
@button-bg:#fff;
@button-color:#333;
@button-font-size:14px;
@button-border-color:#999;
@button-active-bg:#eeeeee;
//type:custom
@button-custom-bg:linear-gradient(66deg,rgba(232,48,56,1),rgba(247,88,151,1));
@button-custom-color:#fff;
@button-custom-font-size:14px;
@button-custom-active-bg:rgba(247,88,151,1);
//size:small
@button-mini-height: 22px;
//size:large
@button-large-height: 36px;
参数
//块级元素
block: {
type: Boolean,
default: () => {
return false
}
},
//类型
type: {
type: String,
default: () => {
return 'default'
},
validator: value => value == 'default' || value == 'custom'
},
//尺寸
size: {
type: String,
default: () => {
return 'common'
},
validator: value =>
value == 'small' || value == 'large' || value == 'common'
},
//是否下载状态
isLoading: {
type: Boolean,
default: () => {
return false
}
},
//icon
iconName: {
type: String,
default: () => {
return ''
}
},
//icon 位置
iconPosition: {
type: String,
default: () => {
return 'left'
},
validator: value => {
return value == 'left' || value == 'right'
}
}
IOS上不支持active伪类
解决:
mounted() {
document.body.addEventListener('touchstart', function() {})
}
做移动端开发的时候遇到ios点击效果会自带背景阴影,去掉阴影的方法:
*{
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color:transparent;
}
-webkit的意思是:IOS浏览器;
-tap的意思是:点击;
-highlight的意思是:背景高亮;
-color的意思是:颜色
rgba(0,0,0,0) 和 transparent这两个是一个意思,就是纯黑透明;
icon组件
使用iconfont 矢量图
- 小红点
- 角标
- 颜色填充 参数
iconName: {
//icon 名称
type: String,
default: () => {
return ''
}
},
dot: {
//右上角的小红点
type: Boolean,
default: () => {
return false
}
},
info: {
//角标
type: [Number, String],
default: () => {
return 0
}
}
color: {
//颜色
type: String,
default: () => {
return ''
}
}
角标样式
.z-badge {
position: absolute;
top: 0;
right: 0;
box-sizing: border-box;
min-width: 16px;
padding: 0 3px;
color: #fff;
font-weight: 500;
font-size: 12px;
line-height: 14px;
text-align: center;
background-color: @z-badge-bg;
border: 1px solid #fff;
border-radius: 16px;
transform: translate(50%, -50%);
transform-origin: 100%;
}
svg设置高度后 父级div会比svg高度高4px左右
原因
SVG主要有3种使用场景
1、作为背景图片使用
2、src引入,作为图片使用
3、直接在标签中使用(内联SVG)
在第3中使用方式中,SVG元素是一个inline类型的标签,浏览器会为标签之间的换行和空格,生成一个看不见的空文本节点,这个空的文本节点占据了位置,增加了一个看不见的高度。
解决方式
1、负边距消除 (margin-bottom: -4px;)
2、改成块级标签(display: block; float等)
cell单元格组件
cell 全局变量
@cell-padding-left-right:12px;
@cell-padding-top-bottom:12px;
左右两边各放置插槽
属性
title:{ //单元格内容
type:String,
default:()=>{
return ""
},
required:true
},
path:{ //路由跳转路径
type:Object,
default:()=>{
return null
}
}
z-input 输入框组件
- 提示
- v-model
- type
- disabled 禁用
- 错误提示
- 直接在组件外部监听组件内input的所有事件
- 清除icon 属性
placeholder:{//提示
type:String,
default:()=>{
return "请输入内容"
}
},
value:{ //输入值
type:[String,Number],
default:()=>{
return ""
},
required:true
},
type:{ //类型
type:String,
default:()=>{
return ""
}
},
disabled:{ //禁用状态
type:Boolean,
default:()=>{
return false
}
},
errorTip:{ //错误提示
type:String,
default:()=>{
return ""
}
}
z-sticky吸顶组件
- 吸顶
- 设置吸顶距离 属性
props: {
offsetTop: { //吸顶时与顶部的距离
type: Number,
default: () => {
return 0
}
}
},
获取最近的滚动的父级容器(来源vant)
getScroll(el, root = window) {
const overflowScrollReg = /scroll|auto/i
let node = el
while (
node &&
node.tagName !== 'HTML' &&
node.nodeType === 1 &&
node !== root
) {
const { overflowY } = window.getComputedStyle(node)
if (overflowScrollReg.test(overflowY)) {
if (node.tagName !== 'BODY') {
return node
}
const { overflowY: htmlOverflowY } = window.getComputedStyle(
node.parentNode
)
if (overflowScrollReg.test(htmlOverflowY)) {
return node
}
}
node = node.parentNode
}
return root
}
判断是否吸顶
滚动距离>=元素距离顶部的距离 - 吸顶时与顶部的距离
//监听滚动事件并设置吸顶
setScrollListener() {
let scrollRoot = this.getScroll(this.$el)
let eleToTop = this.$el.getBoundingClientRect().top
scrollRoot.addEventListener('scroll', () => {
if (scrollRoot.scrollTop >= eleToTop - this.offsetTop) {
this.isSticky = true
} else {
this.isSticky = false
}
//放在下面,不然会出现抖动(吸顶已经浮动,但是添加高度还未0)
this.$emit('scroll', {
scrollTop: scrollRoot.scrollTop,
isSticky: this.isSticky
})
})
},
Tab标签页
- v-model
- tab标签超过4个,标签栏可以在水平方向上滚动,切换时会自动将当前标签居中
- 自定义标签
- 吸顶sticky
props: {
isSticky: {
//是否吸顶
type: Boolean,
default: () => {
return false
}
},
active: {
//激活索引
type: Number,
default: () => {
return 0
},
required: true
},
offsetTop: {
//吸顶距离
type: Number,
default: () => {
return 0
}
}
},
v-model实现思路
示例
<z-tabs v-model="active">
<z-tab title="tab-1">
<div class="tab-content">内容一</div>
</z-tab>
<z-tab title="tab-2">
<div class="tab-content">内容二</div>
</z-tab>
<z-tab title="tab-3">
<div class="tab-content">内容三</div>
</z-tab>
</z-tabs>
配置事件
model: {
prop: 'active',
event: 'change'
},
获取子组件并在每个子组件上监听点击事件,在回调中this.$emit('change', index)发送change事件
//获取子元素列表
let chidren = [];
if (this.isSticky) {//有吸顶的情况
chidren = this.$children[0].$children;
//获取插槽个数
this.length = this.$children[0].$children.length;
} else {
chidren = this.$children;
//获取插槽个数
this.length = this.$children.length;
}
//设置v-model
setVModel(chidren) {
chidren.forEach((element, index) => {
element.$el.onclick = () => {
this.$emit("change", index);
this.$emit("click", { index, title: element.title });
this.reset(chidren);
element.isActive = true;
this.scroll(element);
};
if (index == this.active) {
element.isActive = true;
}
this.setTabContent(index, element);
});
},
设置标签页的内容 (外部使用时是在z-tab组件中插入的,需要把内容重新插入到z-tab组件下面的div中)
遇到的问题:如果插入的时内部使用路由(this.$router)的组件,则会报错提示路由underfined
//创建并添加tabContent组件
setTabContent(index, element) {
let that = this;
let tabContentWrap = this.$el.querySelector(".tab-content-wrap");
let ZTabContent = {
props: {
index: {
type: Number,
default: () => {
return 0;
},
required: true
}
},
render() {
return (
<div style={{ display: this.index == that.active ? "" : "none" }}>
{element.$slots.default}
</div>
);
}
};
let ZTabContentConstructor = Vue.extend(ZTabContent);
let component = new ZTabContentConstructor({
propsData: {
index: index
}
});
let div = document.createElement("div");
tabContentWrap.appendChild(div);
component.$mount(div);
},
切换时会自动将当前标签居中
滚动距离=点击元素距离屏幕左边的大小-屏幕大小的一半+点击元素宽度的一半大小 通过改变滚动条距离左边的距离来滚动,并设置最大值为(tabs.scrollWidth - window_width),最小值为0
//滚动
scroll(element) {
let el = element.$el;
let window_width =
document.documentElement.clientWidth || document.body.clientWidth;
let tabs = this.$el.querySelector(".z-tabs");
let offsetLeft = el.offsetLeft - tabs.scrollLeft;
let half_width = el.offsetWidth / 2;
let half_window_width = window_width / 2;
let scrollL = tabs.scrollLeft;
let totalScrollDistance =
scrollL + (offsetLeft - half_window_width) + half_width;
if (totalScrollDistance > tabs.scrollWidth - window_width) {
totalScrollDistance = tabs.scrollWidth - window_width;
} else if (totalScrollDistance < 0) {
totalScrollDistance = 0;
}
let to_scrollLeft = offsetLeft - half_window_width + half_width;
let params = {
scrollDuration: 1500,
tabs,
to_scrollLeft,
totalScrollDistance,
direction: to_scrollLeft > 0 ? "right" : "left"
};
this.animate(params);
},
滚动动画
- requestAnimationFrame
水水地写下(快速点击会有点滚动问题)
animate({
scrollDuration,
tabs,
to_scrollLeft,
totalScrollDistance,
direction
}) {
let scrollCount = (scrollDuration / 1000) * 15
let stepDistance = to_scrollLeft / scrollCount
function step(newTimestamp) {
if (direction == 'right') {
if (tabs.scrollLeft >= totalScrollDistance) {
tabs.scrollLeft = totalScrollDistance
return
}
} else {
if (tabs.scrollLeft <= totalScrollDistance) {
tabs.scrollLeft = totalScrollDistance
return
}
}
tabs.scrollLeft = (tabs.scrollLeft * 100 + stepDistance * 100) / 100
window.requestAnimationFrame(step)
}
window.requestAnimationFrame(step)
},
头部导航栏组件的开发
<template>
<header class="z-header border-bottom-1px">
<div class="left">
<slot name="left"></slot>
</div>
<div class="center flex justify-center aligin-center">
<div class="title ellipsis " v-if="!customCenter">{{title}}</div>
<template v-else>
<slot name="center"></slot>
</template>
</div>
<div class="right">
<slot name="right"></slot>
</div>
</header>
</template>
<script>
export default {
components: {},
props: {
title: {
type: String,
default: () => {
return "标题";
}
},
// 自定义中心内容
customCenter: {
type: Boolean,
default: () => {
return false;
}
}
},
data() {
return {};
},
computed: {},
watch: {},
methods: {},
created() {},
mounted() {},
updated() {}, //生命周期 - 更新之后
destroyed() {} //生命周期 - 销毁完成
};
</script>
<style lang='less' scoped>
.z-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 45px;
z-index: 200;
background: #fff;
box-sizing: border-box;
.left {
position: absolute;
left: 15px;
top: 0;
height: 100%;
z-index: 101;
}
.center {
width: 100%;
height: 100%;
.title{
height: 100%;
line-height: 45px;
max-width: 150px;
}
}
.right {
position: absolute;
right: 15px;
top: 0;
height: 100%;
z-index: 101;
}
}
</style>
底部导航栏
<template>
<div class="tt-navbar flex">
<template v-for="(item,index) in bottomMenu">
<router-link tag="div" :to="item.path" class="menu-item flex justify-center aligin-center flex-column" :key="index">
<slot :item="item" :active="$route.path.match(item.path)" ></slot>
<div class="mt-6">{{item.name}}</div>
</router-link>
</template>
</div>
</template>
<script>
export default {
components: {},
props: {
bottomMenu: {
type: Array,
default: () => {
return [];
}
}
},
data() {
return {};
},
computed: {},
watch: {},
methods: {},
created() {},
mounted() {},
updated() {}, //生命周期 - 更新之后
destroyed() {} //生命周期 - 销毁完成
};
</script>
<style lang='less' scoped>
//@import url(); 引入公共css类
/* 底部导航栏 */
.tt-navbar {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
background: #fff;
box-sizing: border-box;
border-top: 1px solid #ddd;
z-index: 200;
.menu-item {
flex: 1;
height: 100%;
}
.mt-6 {
margin-top: 6px;
}
}
</style>
使用
<BottomNav :bottomMenu="bottomMenu">
<template v-slot="{item,active}">
<img class="img" :src="active?item.imgSelected:img" alt="">
</template>
</BottomNav>
按钮
.zz-button{
transition: all 0.3 ease;
}
.zz-button:active{
opacity: 0.5;
transform: scale(0.95,0.95);
}
在ios 会出现active失效的问题 解决:
document.body.addEventListener('touchstart',function(){});
开关
.switch {
position: relative;
box-sizing: content-box;
width: 45px;
height: 20px;
border: 1px solid #ccc;
outline: 0;
border-radius: 15px;
transition: all .3s ease;
background-color: rgba(0, 0, 0, 0.1);
/* 去掉webkit内核里默认的样式 */
-webkit-appearance: none;
/* 去掉webkit内核里默认的点击效果 */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
&:checked {
border-color: #07c160;
background-color: #07c160;
}
&::after {
content: " ";
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 20px;
transition: left .3s ease;
border-radius: 50%;
background-color: #ffffff;
box-shadow: 0 0 2px #999;
}
}
.switch:checked.switch::after{
left:25px ;
}
dialog
.content {
z-index: 301;
width: 245px;
height: 400px;
border-radius: 5px;
background: #fff;
animation: show 0.3s 1;
box-shadow: 14px 25px 16px 7px rgba(0, 0, 0, 0.05);
@keyframes show {
0% {
opacity: 0;
transform: scale(1.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
}
列表左滑组件
<!-- -->
<template>
<div class="delete">
<div class="slider">
<div
class="content"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
:style="deleteSlider"
>
<!-- 插槽中放具体项目中需要内容 -->
<slot></slot>
</div>
<div class="remove" ref="remove">删除</div>
</div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
startX: 0, //触摸位置
endX: 0, //结束位置
moveX: 0, //滑动时的位置
disX: 0, //移动距离
deleteSlider: "" //滑动时的效果,使用v-bind:style="deleteSlider"
};
},
computed: {},
watch: {},
methods: {
touchStart(ev) {
ev = ev || event;
//tounches类数组,等于1时表示此时有只有一只手指在触摸屏幕
if (ev.touches.length == 1) {
// 记录开始位置
//clientX 触点相对于可见视区(visual viewport)左边沿的的X坐标. 不包括任何滚动偏移. 只读属性.
this.startX = ev.touches[0].clientX;
}
},
touchMove(ev) {
ev = ev || event;
//获取删除按钮的宽度,此宽度为滑块左滑的最大距离
let wd = this.$refs.remove.offsetWidth;
if (ev.touches.length == 1) {
// 滑动时距离浏览器左侧实时距离
this.moveX = ev.touches[0].clientX;
//起始位置减去 实时的滑动的距离,得到手指实时偏移距离
this.disX = this.startX - this.moveX;
// 如果是向右滑动或者不滑动,不改变滑块的位置
if (this.disX < 0 || this.disX == 0) {
this.deleteSlider = "transform:translateX(0px)";
// 大于0,表示左滑了,此时滑块开始滑动
} else if (this.disX > 0) {
//具体滑动距离我取的是 手指偏移距离*5。
this.deleteSlider = "transform:translateX(-" + this.disX * 5 + "px)";
// 最大也只能等于删除按钮宽度
if (this.disX * 5 >= wd) {
this.deleteSlider = "transform:translateX(-" + wd + "px)";
}
}
}
},
touchEnd(ev) {
ev = ev || event;
let wd = this.$refs.remove.offsetWidth;
if (ev.changedTouches.length == 1) {
/*changedTouches只读属性是TouchList其接触点(Touch对象)根据事件类型而变化,如下所示:
对于该touchstart事件,它是对当前事件变为活动的触摸点列表。
对于该touchmove事件,它是自上次事件以来已更改的触摸点列表。
对于该touchend事件,它是已从表面移除的触摸点的列表(即,与手指不再接触表面的触摸点集合)。*/
let endX = ev.changedTouches[0].clientX;
this.disX = this.startX - endX;
//如果距离小于删除按钮一半,强行回到起点
if (this.disX * 5 < wd / 2) {
this.deleteSlider = "transform:translateX(0px)";
} else {
//大于一半 滑动到最大值
this.deleteSlider = "transform:translateX(-" + wd + "px)";
}
}
}
},
created() {},
mounted() {},
updated() {}, //生命周期 - 更新之后
destroyed() {} //生命周期 - 销毁完成
};
</script>
<style scoped lang="less">
.slider {
width: 100%;
height: 100px;
position: relative;
user-select: none;
.content {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: green;
z-index: 100;
// 设置过渡动画
transition: 0.3s;
}
.remove {
position: absolute;
width: 200px;
height: 100px;
background: red;
right: 0;
top: 0;
color: #fff;
text-align: center;
font-size: 20px;
line-height: 100px;
}
}
</style>
- TouchEvent.touches属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。
ActionSheet
/* 弹出菜单容器,默认隐藏在屏幕的下面 */
.tt-action-sheet > .tt-action-sheet-wrap{
position: fixed;
bottom: 0;
left: 0;
right: 0;
max-width: 640px;
margin: auto;
background: #eee;
transition: transform .3s ease;
transform: translateY(100%);
z-index: 301;
}
/* 菜单弹出的时候,改变容器位移 */
.tt-action-sheet.show .tt-action-sheet-wrap{
transform: translateY(0);
}
弹出菜单这个组件通常是藏在页面下方,不会用的时候再加载,所以要让它一直存在于 DOM 中。这样就会造成一个问题,后面的蒙版层我们可以用透明度 opacity 属性来实现淡入淡出效果。当淡出以后蒙版的透明度是 0,但这个元素还是遮盖着后面的内容区的,导致内容区的操作不能进行。遇到这种情况,就要介绍一下“pointer-events”这个属性了。
auto
- pointer-events 属性默认的取值就是 auto,使用这个属性值的情况下,HTML 元素就是正常的触发点击事件,通常只有为了覆盖不同取值的时候才会使用这个值。
none
- 给元素用上这个属性值的话,这个元素就变成点不中的了,无论这个元素是什么样式,点击事件都会忽略它而去触发它底层元素的点击事件。
/* 默认隐藏蒙版 */
.tt-action-sheet > .tt-mask{
opacity: 0;
/* 屏蔽元素的点击事件 */
pointer-events: none;
transition: opacity .3s ease;
}
/* 菜单弹出的时候显示蒙版 */
.tt-action-sheet.show > .tt-mask{
opacity: 1;
pointer-events: auto;
}
img 去除黑边
img[src=""],img:not([src]){
opacity: 0;
}