10
头图

通过本示例,你将学到什么?

CSS 相关

  1. CSS定位
  2. CSS弹性盒子布局
  3. CSS动画
  4. CSS变量的用法

js相关

  1. 类的封装
  2. DOM的操作与事件的操作
  3. 样式与类的操作
  4. 延迟函数的使用

示例效果

废话少说,先看效果如图所示:

分析小程序的构成

  1. 电梯井(也就是电梯上升或者下降的地方)
  2. 电梯
  3. 电梯门(分为左右门)
  4. 楼层
    4.1 楼层数
    4.2 楼层按钮(包含上升和下降按钮)

有哪些功能

  1. 点击楼层,催动电梯上升或者下降
  2. 电梯到达对应楼层,电梯左右门打开
  3. 门打开之后,里面的美女就出来啦
  4. 提示信息: 本美女就要出来了,请速速来迎接
  5. 按钮会有一个点击选中的效果

根据以上的分析,我们就可以很好的实现电梯小程序啦,接下来让我们进入编码阶段吧。

PS: 这里的楼层数是动态生层的,不过建议值不要设置太大,可以在代码里做限制。

准备工作

创建一个index.html文件,并初始化HTML5文档的基本结构,如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>elevator</title>
    <link rel="stylesheet" href="./styles/style.css">
</head>
<body>
    <!--这里写代码-->
</body>
<script src="https://www.eveningwater.com/static/plugin/message.min.js"></script>
<script src="./scripts/index.js"></script>
<script>
    //这里写代码
</script>
</html>

创建一个styles目录并创建一个style.css文件,初始化代码如下:

//色彩变量
:root {
    --elevatorBorderColor--: rgba(0,0,0.85);
    --elevatorBtnBgColor--: #fff;
    --elevatorBtnBgDisabledColor--: #898989;
    --elevatorBtnDisabledColor--: #c2c3c4;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

如何调用

我们是通过将功能封装在一个Elevator类当中,最终调用如下所示:

//6代表楼层数,不建议设置太大
 new Elevator(6);

初始化工作完成,接下来让我们来看看具体的实现吧。

特别申明一下: 我们在这里使用了弹性盒子布局,请注意浏览器的兼容性问题。

代码实现

构建楼房

首先我们需要一个容器元素,代表是当前的包含电梯的楼房或者是建筑,HTML代码如下:

<div class="ew-elevator-building"></div>

建筑的样式很简单,固定宽,并设置最小高度(因为楼层数是动态生成的,不固定的,所以高度不能够固定),然后设置边框,其它就没有什么了。代码如下:

.ew-elevator-building {
    width: 350px;
    max-width: 100%;
    min-height: 500px;
    border: 6px solid var(--elevatorBorderColor--);
    margin: 3vh auto;
    overflow: hidden;
    display: flex;  
}

构建电梯井

接下来是电梯井,电梯井又包含电梯和电梯左右门,因此HTML文档结构就出来了,如下所示:

<!--电梯井-->
<div class="ew-elevator-shaft">
    <!--电梯-->
    <div class="ew-elevator">
        <!--电梯左右门-->
        <div class="ew-elevator-left-door ew-elevator-door"></div>
        <div class="ew-elevator-right-door ew-elevator-door"></div>
    </div>
</div>

根据效果图,我们可以很快速的写出样式代码,如下所示:

//电梯井,主要就是设置相对定位和边框,固定宽度
.ew-elevator-shaft {
    border-right: 2px solid var(--elevatorBorderColor--);
    width: 200px;
    padding: 1px;
    position: relative;
}

构建电梯

我们来思考一下这里为什么要使用相对定位,因为我们的电梯是上升和下降的,我们可以通过绝对定位加bottom或者top的偏移量来模拟电梯的上升和下降,因此电梯我们就需要设置为绝对定位,而电梯是相对于电梯井偏移的,所以需要设置为相对定位。继续编写样式代码:

.ew-elevator {
    height: 98px;
    width: calc(100% - 2px);
    background: url("https://www.eveningwater.com/my-web-projects/js/26/img/6.jpg") center / cover no-repeat;
    border: 1px solid var(--elevatorBorderColor--);
    padding: 1px;
    transition-timing-function: ease-in-out;
    position: absolute;
    left: 1px;
    bottom: 1px;
}

构建电梯门

可以看到,默认电梯就在第一层,所以bottom偏移量就是1px,并且我们设置一个背景图,代表是里面的人,电梯门开启后就显示该背景图。接下来是电梯门的样式:

.ew-elevator-door {
    position: absolute;
    width: 50%;
    height: 100%;
    background-color: var(--elevatorBorderColor--);
    border: 1px solid var(--elevatorBtnBgColor--);
    top: 0;
}

.ew-elevator-left-door {
    left: 0;
}

.ew-elevator-right-door {
   right: 0;
}

其实也很好理解,就是左右门各占一步,左边的电梯门居左,右边的电梯门居右。接下来是给电梯开门添加动画,添加一个toggle类名就行了:

.ew-elevator-left-door.toggle {
    animation: doorLeft 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);
}

.ew-elevator-right-door.toggle {
    animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);
}

@keyframes doorLeft {
    0% {
        left: 0px;
    }
    25% {
        left: -90px;
    }
    50% {
        left: -90px;
    }
    100% {
        left:0;
    }
}

@keyframes doorRight {
    0% {
        right: 0px;
    }
    25% {
        right: -90px;
    }
    50% {
        right: -90px;
    }
    100% {
        right:0;
    }
}

很显然电梯左边的门是往左边偏移,电梯右边的门是往右边偏移。

构建楼层

接下来是楼层的样式,楼层也包含了两个部分,第一个就是电梯按钮控制,第二个就是楼层数。因此HTML文档结构如下:

<div class="ew-elevator-storey-zone">
    <!--这一块就是楼层,因为是动态生成的,但是我们可以先写第一个,然后写好样式-->
    <div class="ew-elevator-storey">
        <!--电梯按钮,包含上升按钮和下降按钮-->
        <div class="ew-elevator-controller">
           <button type="button" class="ew-elevator-to-top ew-elevator-btn">↑</button>
           <button type="button" class="ew-elevator-to-bottom ew-elevator-btn">↓</button>
        </div>
        <!--楼层数-->
        <div class="ew-elevator-count">1</div>
    </div>
</div>

楼层容器和每个楼层元素的样式很简单没什么可以说的,如下:

//楼层容器元素
.ew-elevator-storey-zone {
    width: auto;
    height: 100%;
}
//楼层元素
.ew-elevator-storey {
    display: flex;
    align-items: center;
    height: 98px;
    border-bottom: 1px solid var(--elevatorBorderColor--);
}

构建电梯按钮

接下来是电梯按钮的容器元素,如下所示:

.ew-elevator-controller {
    width: 70px;
    height: 98px;
    padding: 8px 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
}

都是常规的样式,比如弹性盒子的垂直水平居中,与列换行,即便是按钮元素也没有什么好说的。

//电梯按钮
.ew-elevator-btn {
    width: 36px;
    height: 36px;
    border: 1px solid var(--elevatorBorderColor--);
    border-radius: 50%;
    outline: none;
    cursor: pointer;
    background-color: var(--elevatorBtnBgColor--);
}
//需要给按钮添加一个选中样式效果
.ew-elevator-btn.checked {
    background-color: var(--elevatorBorderColor--);
    color:var(--elevatorBtnBgColor--);
}
//加点上间距
.ew-elevator-btn:last-of-type {
    margin-top: 8px;
}
//按钮禁用
.ew-elevator-btn[disabled] {
    cursor: not-allowed;
    background-color: var(--elevatorBtnBgDisabledColor--);
    color: var(--elevatorBtnDisabledColor--);
}
//楼层数样式
.ew-elevator-count {
    width: 80px;
    height: 98px;
    text-align: center;
    font: 56px / 98px "微软雅黑","楷体";
}

js代码

初始化类

html文档和CSS布局就到此为止了,接下来才是重头戏,也就是功能逻辑的实现,首先我们定义一个类叫Elevator,并初始化它的一些属性,代码如下:

class Elevator {
    // 参数代表楼层数
    constructor(count){
         //总楼层数缓存下来
        this.count = count;
        //当前楼层索引
        this.onFloor = 1;
        //电梯按钮组
        this.btnGroup = null;
        //动态生成楼层数的容器元素
        this.zoneContainer = this.$(".ew-elevator-storey-zone");
        //电梯元素
        this.elevator = this.$(".ew-elevator");
    }
}

添加获取DOM的工具方法

初始化工作完成后,我们需要添加一些工具方法,例如获取DOM元素,我们采用document.querySelector与document.querySelectorAll方法,我们将这两个方法封装一下,于是变成如下的代码:

class Elevator {
    // 参数代表楼层数
    constructor(count){
         //总楼层数缓存下来
        this.count = count;
        //当前楼层索引
        this.onFloor = 1;
        //电梯按钮组
        this.btnGroup = null;
        //动态生成楼层数的容器元素
        this.zoneContainer = this.$(".ew-elevator-storey-zone");
        //电梯元素
        this.elevator = this.$(".ew-elevator");
    }
    //这里是封装的代码
    $(selector, el = document) {
       return el.querySelector(selector);
    }
    $$(selector, el = document) {
      return el.querySelectorAll(selector);
    }
}

动态生成楼层数

接下来,我们要根据传入的参数动态生成楼层数,在构造函数中调用它,因此代码就变成了如下:

class Elevator {
    // 参数代表楼层数
    constructor(count){
         //总楼层数缓存下来
        this.count = count;
        //当前楼层索引
        this.onFloor = 1;
        //电梯按钮组
        this.btnGroup = null;
        //动态生成楼层数的容器元素
        this.zoneContainer = this.$(".ew-elevator-storey-zone");
        //电梯元素
        this.elevator = this.$(".ew-elevator");
        //这里添加了代码
        this.generateStorey(this.count || 6);
    }
    //这里是封装的代码
    $(selector, el = document) {
       return el.querySelector(selector);
    }
    $$(selector, el = document) {
      return el.querySelectorAll(selector);
    }
    //这里添加了代码
    generateStorey(count){
       //这里写逻辑
    }
}

好了,让我们思考一下,我们应该如何生成DOM元素并添加到DOM元素中,很简单,我们可以利用模板字符串的拼接,然后将拼接好的模板添加到容器元素的innerHTML中。如下:

generateStorey方法内部代码:

    let template = "";
    for (let i = count - 1; i >= 0; i--) {
      template += `
                <div class="ew-elevator-storey">
                    <div class="ew-elevator-controller">
                        <button type="button" class="ew-elevator-to-top ew-elevator-btn" ${
                          i === count - 1 ? "disabled" : ""
                        }>↑</button>
                        <button type="button" class="ew-elevator-to-bottom ew-elevator-btn" ${
                          i === 0 ? "disabled" : ""
                        }>↓</button>
                    </div>
                    <div class="ew-elevator-count">${i + 1}</div>
                </div>
            `;
    }
    this.zoneContainer.innerHTML = template;
    this.storeys = this.$$(".ew-elevator-storey", this.zoneContainer);
    this.doors = this.$$(".ew-elevator-door", this.elevator);
    this.btnGroup = this.$$(".ew-elevator-btn", this.zoneContainer);

这里我们需要注意一点,在顶楼是没有上升的操作,因此顶楼的上升按钮需要禁用掉,而底层也没有下降的操作,底楼的下降按钮也要禁用掉,这也是以下代码的意义所在:

i === count - 1 ? "disabled" : "";
i === 0 ? "disabled" : "";

为按钮添加点击事件

我们通过索引来判断,然后给按钮添加disabled属性禁用按钮点击。动态生成完成之后,我们在初始化楼层元素数组和电梯门的元素数组以及按钮元素数组。然后我们就是需要给按钮添加点击事件,继续在generateStorey方法内部添加如下一行代码:

 [...this.storeys].forEach((item, index) => {
      this.handleClick(
        this.$$(".ew-elevator-btn", item),
        item.offsetHeight,
        index
      );
 });

以上代码就是获取每一层楼的电梯按钮,因为偏移量与楼层的高度有关,所以我们获取每个楼层的offsetHeight来确定bottom的偏移量,然后就是每层楼的索引,用来计算偏移量的。

继续看handleClick方法,如下:

handleClick(btnGroup, floorHeight, floorIndex) {
    Array.from(btnGroup).forEach((btn) => {
      btn.addEventListener("click", () => {
        if (btn.classList.contains("checked")) {
          return;
        }
        btn.classList.add("checked");
        const currentFloor = this.count - floorIndex;
        const moveFloor = currentFloor - 1;
        this.elevatorMove(currentFloor, floorHeight * moveFloor);
      });
    });
}

电梯的上升与下降

该方法内部实际上就是为每个按钮添加点击事件,点击首先判断是否被选中,如果选中则不执行,否则添加选中样式然后通过楼层总数减去当前点击的楼层索引在减去1得到当前需要移动的楼层数,然后调用elevatorMove方法,将当前楼层索引和移动的偏移量当做参数传入该方法。接下来我们来看elevatorMove方法。

elevatorMove(num, offset) {
    const currentFloor = this.onFloor;
    const diffFloor = Math.abs(num - currentFloor);

    this.addStyles(this.elevator, {
      transitionDuration: diffFloor + "s",
      bottom: offset + "px",
    });

    Array.from(this.doors).forEach((door) => {
      door.classList.add("toggle");
      this.addStyles(door, {
        animationDelay: diffFloor + "s",
      });
    });

    $message.success(
      `本美女就要出来了,请速速来迎接,再等${
        (diffFloor * 1000 + 3000) / 1000
      }s就关电梯门了!`
    );

    setTimeout(() => {
      [...this.btnGroup].forEach((btn) => btn.classList.remove("checked"));
    }, diffFloor * 1000);

    setTimeout(() => {
      Array.from(this.doors).forEach((door) => door.classList.remove("toggle"));
    }, diffFloor * 1000 + 3000);

    this.onFloor = num;
}

添加样式的工具方法

这个方法我们做了哪些操作呢?首先我们通过楼层索引来计算动画的执行过渡时间,这里就涉及到了一个添加样式的工具方法,代码如下:

addStyles(el, styles) {
  Object.assign(el.style, styles);
}

十分简单,就是通过Object.assign将style和styles合并在一起。

添加了电梯的移动样式,我们就需要为电梯门的开启添加延迟执行时间以及toggle类名执行电梯门开启的动画,然后弹出提示消息本美女就要出来了,请速速来迎接,再等${(diffFloor * 1000 + 3000) / 1000}s就关电梯门了!,然后延迟清除按钮的选中效果,最后延迟移除开启电梯左右门的动画效果类名toggle,并将当前楼层设置一下,一个简单的电梯小程序就完成了。

完整代码

最后我们合并一下代码,完整的js代码就完成了:

class Elevator {
  constructor(count) {
    this.count = count;
    this.onFloor = 1;
    this.btnGroup = null;
    this.zoneContainer = this.$(".ew-elevator-storey-zone");
    this.elevator = this.$(".ew-elevator");
    this.generateStorey(this.count || 6);
  }
  $(selector, el = document) {
    return el.querySelector(selector);
  }
  $$(selector, el = document) {
    return el.querySelectorAll(selector);
  }
  generateStorey(count) {
    let template = "";
    for (let i = count - 1; i >= 0; i--) {
      template += `
                <div class="ew-elevator-storey">
                    <div class="ew-elevator-controller">
                        <button type="button" class="ew-elevator-to-top ew-elevator-btn" ${
                          i === count - 1 ? "disabled" : ""
                        }>↑</button>
                        <button type="button" class="ew-elevator-to-bottom ew-elevator-btn" ${
                          i === 0 ? "disabled" : ""
                        }>↓</button>
                    </div>
                    <div class="ew-elevator-count">${i + 1}</div>
                </div>
            `;
    }
    this.zoneContainer.innerHTML = template;
    this.storeys = this.$$(".ew-elevator-storey", this.zoneContainer);
    this.doors = this.$$(".ew-elevator-door", this.elevator);
    this.btnGroup = this.$$(".ew-elevator-btn", this.zoneContainer);
    [...this.storeys].forEach((item, index) => {
      this.handleClick(
        this.$$(".ew-elevator-btn", item),
        item.offsetHeight,
        index
      );
    });
  }
  handleClick(btnGroup, floorHeight, floorIndex) {
    Array.from(btnGroup).forEach((btn) => {
      btn.addEventListener("click", () => {
        if (btn.classList.contains("checked")) {
          return;
        }
        btn.classList.add("checked");
        const currentFloor = this.count - floorIndex;
        const moveFloor = currentFloor - 1;
        this.elevatorMove(currentFloor, floorHeight * moveFloor);
      });
    });
  }
  elevatorMove(num, offset) {
    const currentFloor = this.onFloor;
    const diffFloor = Math.abs(num - currentFloor);

    this.addStyles(this.elevator, {
      transitionDuration: diffFloor + "s",
      bottom: offset + "px",
    });

    Array.from(this.doors).forEach((door) => {
      door.classList.add("toggle");
      this.addStyles(door, {
        animationDelay: diffFloor + "s",
      });
    });

    $message.success(
      `本美女就要出来了,请速速来迎接,再等${
        (diffFloor * 1000 + 3000) / 1000
      }s就关电梯门了!`
    );

    setTimeout(() => {
      [...this.btnGroup].forEach((btn) => btn.classList.remove("checked"));
    }, diffFloor * 1000);

    setTimeout(() => {
      Array.from(this.doors).forEach((door) => door.classList.remove("toggle"));
    }, diffFloor * 1000 + 3000);

    this.onFloor = num;
  }
  addStyles(el, styles) {
    Object.assign(el.style, styles);
  }
}

然后我们就可以实例化这个类了,如下所示:

new Elevator(6);

最后

当然这里我们还可以扩展,比如楼层数的限制,再比如添加门开启后,里面的美女真的走出来的动画效果,如有兴趣可以参考源码自行扩展。

在线示例

最后谢谢大家观看,如果觉得本文有帮助到你,望不吝啬点赞和收藏,动动小手点star,嘿嘿。

特别声明: 这个小示例只适合新手学习,大佬就算了,这个小程序对大佬来说很简单。

夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。