关于javascript:手写一个有点意思的电梯小程序React版本

60次阅读

共计 20398 个字符,预计需要花费 51 分钟才能阅读完成。

查看成果

咱们先来看一下明天要实现的示例的成果,如下所示:

好,接下来咱们也看到了这个示例的成果,让咱们进入正题,开始欢快的编码吧。

技术栈介绍

这个小程序,咱们将采纳 React + typescript + css in js 语法编写, 并且采纳最新比拟风行的工具 vite 来构建。

初始化我的项目

咱们能够抉择在电脑按住 shift, 而后右键,抉择 powershell,也就是默认的零碎终端。而后输出命令:

mkdir react-elevator

创立一个目录,创立好之后,接着咱们在 vscode 中关上这个目录,关上之后,在 vscode 中关上终端,输出以下命令:

npm init vite@latest react-elevator -- --template react-ts

留神在命令界面,咱们要抉择 react,react-ts。初始化我的项目好了之后,咱们在输出命令:

cd react-elevator
npm install
npm run dev

查看一下咱们初始化我的项目是否胜利。

特地申明: 请留神装置了 node.js 和 npm 工具

css in js

能够看到,咱们的我的项目初始化曾经实现,好,接下来,咱们还要额定的装一些我的项目当中遇到的依赖,例如 css in js, 咱们须要装置 @emotion/styled,@emotion/react 依赖。持续输出命令:

npm install @emotion/styled @emotion/react --save-dev

装置好之后,咱们在我的项目外面应用一下该语法。

首先引入 styled, 如下:

import styled from "@emotion/styled"

接着创立一个款式组件,css in js 实际上就是把每个组件当成一个款式组件,咱们能够通过 styled 前面跟 html 标签名,而后再跟模板字符串,构造如下:

const < 组件名 > = styled.<html 标签名 >`
    // 这里写款式代码
`

例如:

const Link = styled.a`
    color:#fff;
`

以上代码就是写一个字体色彩为红色的超链接组件,而后咱们就能够在 jsx 当中间接写 link 组件。如下所示:

<div>
    <Link> 这是一个超链接组件 </Link>
</div>

当然 emotion 还反对对象写法,然而咱们这里基本上只用模板字符串语法就够了。

接下来步入正题,咱们首先删除初始化的一些代码,因为咱们没有必要用到。

分析程序的构造

好删除之后,咱们接下来看一下咱们要实现的电梯小程序的构造:

  1. 电梯井(也就是电梯回升或者降落的中央)
  2. 电梯
  3. 电梯门(分为左右门)
  4. 楼层
    4.1 楼层数
    4.2 楼层按钮(蕴含回升和降落按钮)

构造好了之后,接下来咱们来看看有哪些性能:

  1. 点击楼层,催动电梯回升或者降落
  2. 电梯达到对应楼层,电梯左右门关上
  3. 门关上之后,外面的美女就进去啦
  4. 按钮会有一个点击选中的成果

咱们先来剖析构造,依据以上的拆分,咱们能够大抵将整个小程序分成如下几个组件:

  1. 楼房(容器组件)
  2. 电梯井组件
    2.1 电梯组件
    2.1.1 电梯右边的门
    2.1.1 电梯左边的门
  3. 楼层组件
    3.1 楼层管制组件
    3.1.1 楼层回升按钮组件
    3.1.2 楼层降落按钮组件
    3.2 楼层数组件

咱们先来写好组件和款式,而后再实现性能。

楼房组件

首先是咱们的楼房组件,咱们新建一个 components 目录,再新建一个 ElevatorBuild.tsx 组件,外面写上如下代码:

import styled from "@emotion/styled"

const StyleBuild = styled.div`
    width: 350px;
    max-width: 100%;
    min-height: 500px;
    border: 6px solid var(--elevatorBorderColor--);
    overflow: hidden;
    display: flex;
    margin: 3vh auto;
`

const ElevatorBuild = () => {
    return (<StyleBuild></StyleBuild>)
}

export default ElevatorBuild

这样,咱们的一个楼房组件就算是实现了,而后咱们在 App.tsx 当中引入,并应用它:

// 这里是新增的代码
import ElevatorBuild from "./components/ElevatorBuild"

const App = () => (
  <div className="App">
    {/* 这里是新增的代码 */}
    <ElevatorBuild />
  </div>
)

export default App

全局款式

在这里,咱们定义了全局 css 变量款式,因而在当前目录下创立 global.css,并在 main.tsx 中引入,而后在该款式文件中写上如下代码:

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

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

电梯井组件

接下来,让咱们持续实现电梯井组件,同样在 components 目录下新建一个 ElevatorShaft.tsx 组件,外面写上如下代码:

import styled from "@emotion/styled"

const StyleShaft = styled.div`
    width: 200px;
    position: relative;
    border-right: 2px solid var(--elevatorBorderColor--);
    padding: 1px;
`

const ElevatorShaft = () => {
    return (<StyleShaft></StyleShaft>)
}

export default ElevatorShaft

而后咱们在楼房组件中引入并应用它, 如下所示:

import styled from "@emotion/styled"
// 这里是新增的代码
import ElevatorShaft from "./ElevatorShaft"

const StyleBuild = styled.div`
    width: 350px;
    max-width: 100%;
    min-height: 500px;
    border: 6px solid var(--elevatorBorderColor--);
    overflow: hidden;
    display: flex;
    margin: 3vh auto;
`

const ElevatorBuild = () => {
    return (
        <StyleBuild>
            {/* 这里是新增的代码 */}
            <ElevatorShaft></ElevatorShaft>
        </StyleBuild>
    )
}

export default ElevatorBuild

电梯门组件

接着咱们来实现电梯门组件,咱们能够看到电梯门组件有一些公共的款式局部,所以咱们能够抽取进去,新建一个 Door.tsx, 写上如下代码:


import styled from '@emotion/styled';

const StyleDoor = styled.div`
    width:50%;
    position: absolute;
    top: 0;
    height: 100%;
    background-color: var(--elevatorBorderColor--);
    border: 1px solid var(--elevatorBtnBgColor--);
`;

const StyleLeftDoor = styled(StyleDoor)`
    left: 0;
`;

const StyleRightDoor = styled(StyleDoor)`
    right: 0;
`;


export {StyleLeftDoor,StyleRightDoor}

因为咱们性能会须要设置这两个组件的款式,并且咱们这个款式是设置在 style 属性上的,因而咱们能够通过 props 来传递,当初咱们先写好 typescript 接口类,创立一个 type 目录,新建 style.d.ts 全局接口文件,并写上如下代码:

export interface StyleProps {style: CSSProperties}

电梯组件

接下来,咱们就能够开始写电梯组件,如下所示:

import styled from "@emotion/styled"

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

const Elevator = (props: Partial<ElevatorProps>) => {
    return (
        <StyleElevator>
     
        </StyleElevator>
    )
}

export default Elevator

接下来,咱们来看两个电梯门组件,首先是右边的门,如下所示:

import {StyleProps} from "../type/style"
import {StyleLeftDoor} from "./Door"

const ElevatorLeftDoor = (props: Partial<StyleProps>) => {const { style} = props
    return (<StyleLeftDoor style={style}></StyleLeftDoor>
    )
}

export default ElevatorLeftDoor

Partial 是一个泛型,传入接口,代表将接口的每个属性变成可选属性,依据这个原理,咱们能够得悉左边门的组件代码也很相似。如下:

import {StyleProps} from '../type/style';
import {StyleRightDoor} from './Door'
const ElevatorRightDoor = (props: Partial<StyleProps>) => {const { style} = props;
    return (<StyleRightDoor style={style}/>
    )
}

export default ElevatorRightDoor;

这两个组件写好之后,咱们接下来要在电梯组件里引入并应用它们,因为性能逻辑会须要设置款式,因而,咱们通过 props 再次传递 style。如下所示:

import styled from "@emotion/styled"
import {StyleProps} from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

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

export interface ElevatorProps {leftDoorStyle: StyleProps['style'];
    rightDoorStyle: StyleProps['style'];
}

const Elevator = (props: Partial<ElevatorProps>) => {const { leftDoorStyle,rightDoorStyle} =  props;
    return (
        <StyleElevator>
            <ElevatorLeftDoor style={leftDoorStyle} />
            <ElevatorRightDoor style={rightDoorStyle} />
        </StyleElevator>
    )
}

export default Elevator

实现了电梯组件之后,接下来咱们在电梯井组件外面引入电梯组件,留神这里后续逻辑咱们会设置电梯组件和电梯门组件的款式,因而在电梯井组件中,咱们须要通过 props 传递款式。

import styled from "@emotion/styled"
import {StyleProps} from "../type/style";
import Elevator from "./Elevator"


const StyleShaft = styled.div`
    width: 200px;
    position: relative;
    border-right: 2px solid var(--elevatorBorderColor--);
    padding: 1px;
`

export interface ElevatorProps {leftDoorStyle: StyleProps['style'];
    rightDoorStyle: StyleProps['style'];
    elevatorStyle: StyleProps['style'];
}

const ElevatorShaft = (props: Partial<ElevatorProps>) => {const { leftDoorStyle,rightDoorStyle,elevatorStyle} = props;
    return (
        <StyleShaft>
            <Elevator style={elevatorStyle} leftDoorStyle={leftDoorStyle} rightDoorStyle={rightDoorStyle}></Elevator>
        </StyleShaft>
    )
}

export default ElevatorShaft

电梯门组件的开启动画

咱们能够看到,当达到肯定工夫,电梯门会有开启动画,这里咱们显然没有加上,所以咱们能够为电梯门各自加一个是否开启的 props 用来传递,持续批改 Door.tsx 如下:


import styled from '@emotion/styled';

const StyleDoor = styled.div`
    width:50%;
    position: absolute;
    top: 0;
    height: 100%;
    background-color: var(--elevatorBorderColor--);
    border: 1px solid var(--elevatorBtnBgColor--);
`;

const StyleLeftDoor = styled(StyleDoor)<{toggle?:boolean}>`
    left: 0;
    ${({toggle}) => toggle ? 'animation: doorLeft 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;}
    }
`;

const StyleRightDoor = styled(StyleDoor)<{toggle?:boolean}>`
    right: 0;
    ${({toggle}) => toggle ? 'animation: doorRight 3s 1s cubic-bezier(0.075, 0.82, 0.165, 1);' : '' };
    @keyframes doorRight {
        0% {right: 0px;}
        25% {right: -90px;}
        50% {right: -90px;}
        100% {right:0;}
    }
`;


export {StyleLeftDoor,StyleRightDoor}

emotion 语法能够通过函数来返回一个 css 属性,从而达到动静设置属性的目标,一对尖括号,其实也就是 typescript 中的泛型,代表是否传入 toggle 数据,接下来批改 ElevatorLeftDoor.tsx 和 ElevatorRightDoor.tsx。如下:

import {StyleProps} from "../type/style";
import {StyleLeftDoor} from "./Door"

export interface ElevatorLeftDoorProps extends StyleProps {toggle: boolean}

const ElevatorLeftDoor = (props: Partial<ElevatorLeftDoorProps>) => {const { style,toggle} = props;
    return (<StyleLeftDoor style={style} toggle={toggle}></StyleLeftDoor>
    )
}

export default ElevatorLeftDoor
import {StyleProps} from '../type/style'
import {StyleRightDoor} from './Door'

export interface ElevatorRightDoorProps extends StyleProps {toggle: boolean}

const ElevatorRightDoor = (props: Partial<ElevatorRightDoorProps>) => {const { style,toggle} = props;
    return (<StyleRightDoor style={style} toggle={toggle} />
    )
}

export default ElevatorRightDoor

批改电梯和电梯井组件

同样的咱们也须要批改电梯组件和电梯井组件,如下所示:

import styled from "@emotion/styled"
import {StyleProps} from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

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

export interface ElevatorProps extends StyleProps {leftDoorStyle: StyleProps['style']
    rightDoorStyle: StyleProps['style']
    leftToggle: boolean
    rightToggle: boolean
}

const Elevator = (props: Partial<ElevatorProps>) => {const { leftDoorStyle,rightDoorStyle,leftToggle,rightToggle} =  props;
    return (
        <StyleElevator>
            <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
            <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
        </StyleElevator>
    )
}

export default Elevator
import styled from "@emotion/styled";
import {StyleProps} from "../type/style";
import Elevator from "./Elevator";

const StyleShaft = styled.div`
  width: 200px;
  position: relative;
  border-right: 2px solid var(--elevatorBorderColor--);
  padding: 1px;
`;

export interface ElevatorProps {leftDoorStyle: StyleProps["style"];
  rightDoorStyle: StyleProps["style"];
  elevatorStyle: StyleProps["style"];
  leftToggle: boolean;
  rightToggle: boolean;
}

const ElevatorShaft = (props: Partial<ElevatorProps>) => {
  const {
    leftDoorStyle,
    rightDoorStyle,
    elevatorStyle,
    leftToggle,
    rightToggle,
  } = props;
  return (
    <StyleShaft>
      <Elevator
        style={elevatorStyle}
        leftDoorStyle={leftDoorStyle}
        rightDoorStyle={rightDoorStyle}
        leftToggle={leftToggle}
        rightToggle={rightToggle}
      ></Elevator>
    </StyleShaft>
  );
};

export default ElevatorShaft;

然而别忘了咱们这里的电梯组件因为须要回升和降落,因而还须要设置款式,再次批改一下电梯组件的代码如下:

import styled from "@emotion/styled"
import {StyleProps} from "../type/style";
import ElevatorLeftDoor from "./ElevatorLeftDoor"
import ElevatorRightDoor from "./ElevatorRightDoor"

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

export interface ElevatorProps extends StyleProps {leftDoorStyle: StyleProps['style']
    rightDoorStyle: StyleProps['style']
    leftToggle: boolean
    rightToggle: boolean
}

const Elevator = (props: Partial<ElevatorProps>) => {const { style,leftDoorStyle,rightDoorStyle,leftToggle,rightToggle} =  props;
    return (<StyleElevator style={style}>
            <ElevatorLeftDoor style={leftDoorStyle} toggle={leftToggle} />
            <ElevatorRightDoor style={rightDoorStyle} toggle={rightToggle}/>
        </StyleElevator>
    )
}

export default Elevator

楼层容器组件

到目前为止,咱们的左半边局部曾经实现了,接下来,咱们来实现右半边局部的楼层数和管制按钮组件,咱们的楼层是动静生成的,因而咱们须要一个容器组件包裹起来,先写这个楼层容器组件,如下所示:

import styled from "@emotion/styled"

const StyleStoreyZone = styled.div`
    width: auto;
    height: 100%;
`

const Storey = () => {
    return (
        <StyleStoreyZone>
            
        </StyleStoreyZone>
    )
}

export default Storey

楼层组件

能够看到楼层容器组件还是比较简单的,接下来咱们来看楼层组件。如下所示:

import styled from "@emotion/styled";
import {createRef, useEffect, useState} from "react";
import useComponentDidMount from "../hooks/useComponentDidMount";

const StyleStorey = styled.div`
  display: flex;
  align-items: center;
  height: 98px;
  border-bottom: 1px solid var(--elevatorBorderColor--);
`;

const StyleStoreyController = styled.div`
  width: 70px;
  height: 98px;
  padding: 8px 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
`;

const StyleStoreyCount = styled.div`
  width: 80px;
  height: 98px;
  text-align: center;
  font: 56px / 98px 微软雅黑, 楷体;
`;

const StyleButton = styled.button`
  width: 36px;
  height: 36px;
  border: 1px solid var(--elevatorBorderColor--);
  border-radius: 50%;
  outline: none;
  cursor: pointer;
  background-color: var(--elevatorBtnBgColor--);
  &:last-of-type {margin-top: 8px;}
  &.checked {background-color: var(--elevatorBorderColor--);
    color: var(--elevatorBtnBgColor--);
  }
  &[disabled] {
    cursor: not-allowed;
    background-color: var(--elevatorBtnBgDisabledColor--);
    color: var(--elevatorBtnDisabledColor--);
  }
`;

export interface MethodProps {onUp(v: number, t: number, h?: number): void;
  onDown(v: number, t: number, h?: number): void;
}

export interface StoreyProps extends MethodProps{count: number}

export interface StoreyItem {
   key: string
   disabled: boolean
}

const Storey = (props: Partial<StoreyProps>) => {const { count = 6} = props;
  const storeyRef = createRef<HTMLDivElement>();
  const [storeyList, setStoreyList] = useState<StoreyItem []>();
  const [checked, setChecked] = useState<string>();
  const [type, setType] = useState<keyof MethodProps>();
  const [offset,setOffset] = useState(0)
  const [currentFloor, setCurrentFloor] = useState(1);
  useComponentDidMount(() => {let res: StoreyItem [] = [];
    for (let i = count - 1; i >= 0; i--) {
      res.push({key: String(i + 1),
        disabled: false
      });
    }
    setStoreyList(res);
  });

  useEffect(() => {if(storeyRef){setOffset(storeyRef.current?.offsetHeight as number)
    }
  },[storeyRef])

  const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {setChecked(key)
    setType(method)
    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    setCurrentFloor(moveFloor)    
    props[method]?.(diffFloor, offset * (moveFloor - 1))
    // 兴许这不是一个好的办法
    if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true})))
    }
    setTimeout(() => {setChecked(void 0);
      if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false})))
      }
    }, diffFloor * 1000);
  };
  return (
    <>
      {storeyList?.map((item,index) => (<StyleStorey key={item.key} ref={storeyRef}>
          <StyleStoreyController>
            <StyleButton
              disabled={Number(item.key) === storeyList.length || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onUp')}
              className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
            >
              ↑
            </StyleButton>
            <StyleButton
              disabled={Number(item.key) === 1 || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onDown')}
              className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
            >
              ↓
            </StyleButton>
          </StyleStoreyController>
          <StyleStoreyCount>{item.key}</StyleStoreyCount>
        </StyleStorey>
      ))}
    </>
  );
};

export default Storey;

能够看到楼层组件的逻辑十分多,但其实一项一项的剖析下来也并不难。

接下来,咱们在该容器组件中引入,并且将该组件在楼房组件中引入,就能够失去咱们整个电梯小程序的构造了。

在这里咱们来一步一步的剖析楼层组件的逻辑,

楼层数

首先楼层是动静生成的,通过父组件传递,因而咱们在 props 当中定义一个 count,默认值是 6,代表默认生成的楼层数。这也就是咱们这行代码的意义:

export interface StoreyProps extends MethodProps{count: number}
const {count = 6} = props;

楼层的回升与降落

其次咱们在对电梯进行回升和降落的时候,须要获取到每一层楼高,实际上也就是楼层容器元素的高度,如何获取 DOM 元素的理论高度?咱们先想一下,如果是一个实在的 DOM 元素,咱们只须要获取 offsetHeight 就行了,即:

// 这里的 el 显然是一个 dom 元素
const offset: number = el.offsetHeight;

在 react 中,咱们应该如何获取实在的 DOM 元素呢?利用 ref 属性,首先导入 createRef 办法,创立一个 storeyRef, 而后将该 storeyRef 绑定到组件容器元素上,即:

const storeyRef = createRef<HTMLDivElement>();
//...
<StyleStorey ref={storeyRef}></StyleStorey>

而后咱们就能够应用 useEffect 办法,也就是 react hooks 中的一个生命周期钩子函数,监听这个 storeyRef,如果监听到了,就能够间接拿到 dom 元素,并且应用一个状态来存储高度值。即:

const [offset,setOffset] = useState(0)
//...
useEffect(() => {
    //storeyRef.current 显然就是咱们理论拿到的 DOM 元素
    if(storeyRef){setOffset(storeyRef.current?.offsetHeight as number)
    }
},[storeyRef])

楼层列表渲染

接下来,咱们来看楼层数的动静生成,咱们晓得在 react 中动静生成列表元素,实际上就是应用数组的 map 办法,因而咱们要依据 count 来生成一个数组,在这里,咱们能够生成一个 key 数组,然而因为咱们要管制按钮的禁用,因而额定增加一个 disabled 属性,因而这也是以下代码的意义:

export interface StoreyItem {
   key: string
   disabled: boolean
}
const [storeyList, setStoreyList] = useState<StoreyItem []>();
useComponentDidMount(() => {let res: StoreyItem [] = [];
    for (let i = count - 1; i >= 0; i--) {
      res.push({key: String(i + 1),
        disabled: false
      });
    }
    setStoreyList(res);
});

这里波及到一个模仿 useComponentDidMount 钩子函数,很简略,在 hooks 目录下新建一个 useComponentDidMount.ts, 而后写上如下代码:

import {useEffect} from 'react';
const useComponentDidMount = (onMountHandler: (...args:any) => any) => {useEffect(() => {onMountHandler();
  }, []);
};
export default useComponentDidMount;

而后就是渲染楼层组件,如下:

(
    <>
      {storeyList?.map((item,index) => (<StyleStorey key={item.key} ref={storeyRef}>
          <StyleStoreyController>
            <StyleButton
              disabled={Number(item.key) === storeyList.length || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onUp')}
              className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
            >
              ↑
            </StyleButton>
            <StyleButton
              disabled={Number(item.key) === 1 || item.disabled}
              onClick={() => onClickHandler(item.key,index,'onDown')}
              className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}
            >
              ↓
            </StyleButton>
          </StyleStoreyController>
          <StyleStoreyCount>{item.key}</StyleStoreyCount>
        </StyleStorey>
      ))}
    </>
);

<></> 是 React.Fragment 的一种写法,能够了解它就是一个占位标签,没有什么理论含意,storeyList 默认值是 undefined,因而须要加? 代表可选链,这里蕴含了两个局部,第一个局部就是管制按钮,第二局部就是显示楼层数。

实际上咱们生成的元素数组中的 key 就是楼层数,这也是这行代码的意义:

<StyleStoreyCount>{item.key}</StyleStoreyCount>

还有就是 react 在生成列表的时候,须要绑定一个 key 属性,不便虚构 DOM,diff 算法的计算,这里不必多讲。接下来咱们来看按钮组件的逻辑。

楼层按钮组件

按钮组件的逻辑蕴含三个局部:

  1. 禁用成果
  2. 点击使得电梯回升和降落
  3. 选中成果

咱们晓得最高楼的回升是无奈回升的,所以须要禁用,同样的,底楼的降落也是须要禁用的, 所以这两行代码就是这个意思:

Number(item.key) === storeyList.length
Number(item.key) === 1

接下来还有一个条件,这个 item.disabled 其实次要是避免反复点击的问题,当然这并不是一个好的解决办法,但目前来说咱们先这样做,定义一个 type 状态,代表是点击的回升还是降落:

// 接口类型,type 应只能是 onUp 或者 onDown, 代表只能是回升还是降落
export interface MethodProps {onUp(v: number, t: number, h?: number): void;
  onDown(v: number, t: number, h?: number): void;
}
const [type, setType] = useState<keyof MethodProps>();

而后定义一个 checked 状态,代表以后按钮是否选中:

//checked 存储 key 值,所以
const [checked, setChecked] = useState<string>();
className={`${item.key === checked && type === 'onUp' ? "checked" : ""}`}
className={`${item.key === checked && type === 'onDown' ? "checked" : ""}`}

须要留神的就是这里的判断:

item.key === checked && type === 'onUp' // 以及 ${item.key === checked && type === 'onDown'

咱们款式当中是增加了 checked 的,这个没什么好说的。

而后,咱们还须要缓存以后楼层是哪一楼,因为下次点击的时候,咱们就须要依据以后楼层来计算,而不是重头开始。

const [currentFloor, setCurrentFloor] = useState(1);

最初,就是咱们的点击回升和降落逻辑,还是有点简单的:

const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {setChecked(key)
    setType(method)
    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    setCurrentFloor(moveFloor)    
    props[method]?.(diffFloor, offset * (moveFloor - 1))
    // 兴许这不是一个好的办法
    if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true})))
    }
    setTimeout(() => {setChecked(void 0);
      if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false})))
      }
    }, diffFloor * 1000);
};

该函数有三个参数,第一个代表以后楼层的 key 值,也就是楼层数,第二个代表以后楼的索引,留神索引和楼层数是不一样的,第三个就是点击的是回升还是降落。咱们的第一个参数和第三个参数是用来设置按钮的选中成果,即:

setChecked(key)
setType(method)

接下来,咱们须要计算动画的执行工夫,例如咱们从第一层到第五层,如果按每秒到一层来计算,那么第一层到第五层就须要 4s 的工夫,同理咱们的偏移量就应该是每层楼高与须要挪动的楼高在减去 1。因而,计算须要挪动的楼高咱们是:

const moveFloor = count - index
const diffFloor = Math.abs(moveFloor - currentFloor)
// 设置以后楼层
setCurrentFloor(moveFloor) 
props[method]?.(diffFloor, offset * (moveFloor - 1))

留神咱们是将事件抛给父组件的,因为咱们的电梯组件和楼层容器组件在同一层级,只有这样,能力设置电梯组件的款式。即:

// 传入两个参数,代表动画的执行工夫和偏移量,props[method] 其实就是动静获取 props 中的属性
props[method]?.(diffFloor, offset * (moveFloor - 1))

而后这里的逻辑就是避免反复点击的代码,这并不是一个好的解决形式:

if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: true})))
}
setTimeout(() => {
    //...
    if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: false})))
    }
}, diffFloor * 1000);

批改楼层容器组件

好了,这个组件的剖析就到此为止了,咱们既然把事件抛给了父组件,因而咱们还须要批改一下它的父组件 StoreyZone.tsx 的代码,如下:

import styled from "@emotion/styled";
import Storey from "./Storey";

const StyleStoreyZone = styled.div`
  width: auto;
  height: 100%;
`;
export interface StoreyZoneProps {onUp(v: number, h?: number): void;
  onDown(v: number, h?: number): void;
}
const StoreyZone = (props: Partial<StoreyZoneProps>) => {const { onUp, onDown} = props;
  return (
    <StyleStoreyZone>
      <Storey
        onUp={(k: number, h: number) => onUp?.(k, h)}
        onDown={(k: number, h: number) => onDown?.(k, h)}
      />
    </StyleStoreyZone>
  );
};

export default StoreyZone;

而后就是最初的 ElevatorBuild.tsx 组件的批改, 如下:

import styled from "@emotion/styled";
import {useState} from "react";
import {StyleProps} from "../type/style";
import ElevatorShaft from "./ElevatorShaft";
import StoreyZone from "./StoreyZone";

const StyleBuild = styled.div`
  width: 350px;
  max-width: 100%;
  min-height: 500px;
  border: 6px solid var(--elevatorBorderColor--);
  overflow: hidden;
  display: flex;
  margin: 3vh auto;
`;

const ElevatorBuild = () => {const [elevatorStyle, setElevatorStyle] = useState<StyleProps["style"]>();
  const [doorStyle, setDoorStyle] = useState<StyleProps["style"]>();
  const [open,setOpen] = useState(false)
  const move = (diffFloor: number, offset: number) => {
    setElevatorStyle({
      transitionDuration: diffFloor + 's',
      bottom: offset,
    });
    setOpen(true)
    setDoorStyle({animationDelay: diffFloor + 's'});

    setTimeout(() => {setOpen(false)
    },diffFloor * 1000 + 3000)
  };
  return (
    <StyleBuild>
      <ElevatorShaft
        elevatorStyle={elevatorStyle}
        leftDoorStyle={doorStyle}
        rightDoorStyle={doorStyle}
        leftToggle={open}
        rightToggle={open}
      ></ElevatorShaft>
      <StoreyZone onUp={(k: number,h: number) => move(k,h)} onDown={(k: number,h: number) => move(k,h)}></StoreyZone>
    </StyleBuild>
  );
};

export default ElevatorBuild;

最初,咱们来检查一下代码,看看还有没有什么能够优化的中央,能够看到咱们的按钮禁用逻辑是能够复用的,咱们再从新创立一个函数,即:

const changeButtonDisabled = (key:string,status: boolean) => {if(+key !== storeyList?.length && +key !== 1){setStoreyList((storey) => storey?.map(item => ({ ...item,disabled: status})))
    }
}

const onClickHandler = (key: string,index:number,method: keyof MethodProps) => {
    //...
    changeButtonDisabled(key,true)
    setTimeout(() => {
      //...
      changeButtonDisabled(key,false)
    }, diffFloor * 1000);
};

到此为止,咱们的一个电梯小程序就算是实现了,也不算是简单,总结一下咱们学到的知识点:

  1. css in js 咱们应用的是 @emotion 这个库
  2. 父子组件的通信,应用 props
  3. 操作 DOM,应用 ref
  4. 组件内的状态通信,应用 useState,以及如何批改状态,有两种形式
  5. 钩子函数 useEffect
  6. 类名的操作与事件还有就是款式的设置
  7. React 列表渲染
  8. typescript 接口的定义,以及一些内置的类型

最初

当然这里咱们还能够扩大,比方楼层数的限度,再比方增加门开启后,外面的美女真的走进去的动画成果,如有趣味能够参考源码自行扩大。

在线示例

最初谢谢大家观看,如果感觉本文有帮忙到你,望不悭吝点赞和珍藏,动动小手点 star, 嘿嘿。

特地申明: 这个小示例只适宜老手学习,大佬就算了,这个小程序对大佬来说很简略。

正文完
 0