利用React实现一个有点意思的电梯小程序

目录
  • 查看效果
  • 技术栈介绍
    • 初始化项目
    • css in js
    • 分析程序的结构
    • 楼房组件
    • 全局样式
    • 电梯井组件
    • 电梯门组件
    • 电梯组件
    • 电梯门组件的开启动画
    • 修改电梯和电梯井组件
    • 楼层容器组件
    • 楼层组件
    • 楼层数
    • 楼层的上升与下降
    • 楼层列表渲染
    • 楼层按钮组件
    • 修改楼层容器组件
  • 最后

查看效果

我们先来看一下今天要实现的示例的效果,如下所示

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

技术栈介绍

这个小程序,我们将采用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.电梯井组件

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算法的计算,这里不用多讲。接下来我们来看按钮组件的逻辑。

楼层按钮组件

按钮组件的逻辑包含三个部分:

  • 禁用效果
  • 点击使得电梯上升和下降
  • 选中效果

我们知道最高楼的上升是无法上升的,所以需要禁用,同样的,底楼的下降也是需要禁用的,所以这两行代码就是这个意思:

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);
};

到此为止,我们的一个电梯小程序就算是完成了,也不算是复杂,总结一下我们学到的知识点:

  • css in js 我们使用的是@emotion这个库
  • 父子组件的通信,使用props
  • 操作DOM,使用ref
  • 组件内的状态通信,使用useState,以及如何修改状态,有两种方式
  • 钩子函数useEffect
  • 类名的操作与事件还有就是样式的设置
  • React列表渲染
  • typescript接口的定义,以及一些内置的类型

最后

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

以上就是利用React实现一个有点意思的电梯小程序的详细内容,更多关于React电梯小程序的资料请关注我们其它相关文章!

(0)

相关推荐

  • JQuery实现电梯导航效果

    本文实例为大家分享了JQuery实现电梯导航效果的具体代码,供大家参考,具体内容如下 分享一个基于JQuery实现的电梯导航效果,效果如下: 以下是代码实现: <!DOCTYPE html> <html lang="en">   <head>     <meta charset="utf-8" />     <title>基于JQuery实现电梯导航特效</title>     <styl

  • Python模拟简单电梯调度算法示例

    本文实例讲述了Python模拟简单电梯调度算法.分享给大家供大家参考,具体如下: 经常在公司坐电梯,由于楼层较高,是双联装的电梯,但是经常等电梯很久,经常有人骂写电梯调度算法的.回来闲来无事,自己尝试写了一个简单的. 场景很简单,每一层电梯口只有一个按钮,不区分上下,当有人按下这个键后,电梯会过来停在此层,这个人可以进去,并选择自己想去的层.电梯的调度策略也很简单,在一次向上的过程中,如果有人在下面按了键,电梯并不直接向下,而是运行到此次向上的最顶层,然后再下次向下运行的过程中去服务这个请求.

  • C#控制台模拟电梯工作原理

    每天上下楼都是乘坐电梯的,就想电梯的工作原理是什么呢?于是自己写了个控制台程序来模拟一下电梯的工作原理! 采用面向对象的编程思想!将电梯拆解为两部分: 第一部分就是每个楼层的控制器(每个楼层都有叫梯按钮的哈,一个向上一个向下) 第二部分就电梯间了.电梯间里有楼层按钮,你想上那个楼层就可以按哪个按钮了! 技术难点:状态刷新.命令顺序.电梯运行 核心代码一: using System; using System.Collections.Generic; using System.Linq; usin

  • 基于Java的电梯系统实现过程

    一.思路 写一个简单的电梯系统,首先根据老师提供的需求,写一下基础思路: 电梯有最高层和最低层,输入数字选择正确楼层数 输入数字大于当前楼层,则为上行:小于当前楼层,则为下行 每次输入数字的时候,需要对同为上行的数字或者同为下行的数字,进行排序 所输入的目标楼层用集合存放,循环最低层到最高层,如果当前层在集合中存在,显示开门,若还有目标楼层,则关门,继续到下一目标楼层. 当选择一个目标楼层,会生成随机重量记录在目标楼层,上行用原来重量加上目标楼层重量,下行则用原来重量减去目标楼层重量 二.实现

  • 利用React实现一个有点意思的电梯小程序

    目录 查看效果 技术栈介绍 初始化项目 css in js 分析程序的结构 楼房组件 全局样式 电梯井组件 电梯门组件 电梯组件 电梯门组件的开启动画 修改电梯和电梯井组件 楼层容器组件 楼层组件 楼层数 楼层的上升与下降 楼层列表渲染 楼层按钮组件 修改楼层容器组件 最后 查看效果 我们先来看一下今天要实现的示例的效果,如下所示 好,接下来我们也看到了这个示例的效果,让我们进入正题,开始愉快的编码吧. 技术栈介绍 这个小程序,我们将采用React + typescript + css in j

  • 利用Java编写一个出敬业福的小程序

    目录 1.前言 2.定义工具类 3.生成"福"主类 4.运行测试 5.素材图片 1.前言 “福”的由来: 姜太公封一大批神仙时,却把自己的妻子叶氏封为穷神,还告诉她说:“有福的地方,你不能去.”从此,家家过年贴福字,就是告诉穷神,我这里是有福的地方,你千万不能进来.福字,就是摆脱穷困.追求幸福的象征. 福字之所以倒贴,传说起于清代恭亲王府.那年春节前夕,大管家按例写了几个斗大的“福”字,叫人贴于王府的大门上.有个家丁目不识丁,竟将“福”字头朝下贴上.恭亲王福晋十分气恼,欲鞭罚惩戒.可这

  • 利用c++写一个简单的推箱子小游戏

    效果图 相信各位都肯定完整这种推箱子的小游戏.游戏玩法很简单,那就是一个人把所有的箱子推动到对应的位置那就可以赢了. 那么我们接下来看看这个推箱子的游戏改怎么写 char map[10][10]= { {'#','#','#','#','#','#','#','#','#','#'}, {'#','#','#','#',' ',' ','!',' ',' ','#'}, {'#',' ',' ',' ',' ','o',' ',' ',' ','#'}, {'#',' ',' ',' ',' '

  • 基于Python的一个自动录入表格的小程序

    ## 帮阿雪写的一个小程序 --------------------------------------------------------------------------------------------------- 上大学的时候,总是会由很多表格需要同学们去搞,尤其是刚开学的那个时候,显然是很烦躁, 阿雪刚开学的时候,作为班干部,表示有时候刚录表不是很熟悉经常会弄到很晚,甚至还会弄错, 这就让我很是触动,所以想帮她搞一搞,顺便增强一下我们的友谊/hhhhhh ------------

  • 用C编写一个送给女朋友的情人节小程序 可爱!

    本文实例为大家分享了C编写送给女朋友的小程序,供大家参考,具体内容如下 #include<iostream> #include<conio.h> #include<windows.h> #include<time.h> #include<stdio.h> using namespace std; #define wide 49 #define gao 24 #define high 6 int yanhua[gao][wide],hang,lie

  • 一个小时快速搭建微信小程序的方法步骤

    「小程序」这个划时代的产品发布快一周了,互联网技术人都在摩拳擦掌,跃跃欲试.可是小程序目前还在内测,首批只发放了 200 个内测资格(泪流满面).本以为没有 AppID 这个月就与小程序无缘了,庆幸的是微信这两天发布了正式版开发者工具,无需内测邀请也可以尝鲜了. 因此也就有了我与「小程序」的初体验,而我的感受只有一个字--爽! 选择哪个「小程序」Demo? 在知名同性交友网站 Github 上,「小程序」的 Demo 不少,但是大多只是简单的 API 演示,有的甚至直接把页面数据写在了 json

  • 微信小程序如何利用getCurrentPages进行页面传值

    最近刚赶完项目,利用空闲时间总结一下. 小程序的页面间传值 , 之前处理这种例如 a页面跳转b页面,在b页面进行一波操作 回到a页面 都是把b页面的操作的数据存到本地存储 wx.setStorageSync("b_data","b页面的数据") 在a页面是这样的 wx.getStorageSync("b_data") 但是这种方法怎么说呢 不利于操作 还会导致storage里面的数据非常混乱过一段时间鬼知道是什么,操作也麻烦 总之就是略low 后

  • 利用Webpack实现小程序多项目管理的方法

    故事是这样的 产品小姐姐:"我要做一堆小程序,一周上线一到两个没问题吧" 码畜小哥哥:"你他喵是不是傻,做那么多干什么" 产品小姐姐:"蹭些流量呀,用户量多了就可以考虑转化流量给公司的 App" 码畜小哥哥:"fuck 好的" 码畜小哥开始架构 小程序杂,放一个项目方便管理 小程序多,代码要能够复用 团队开发,代码风格要统一 码畜小哥开始建项目 这是单个小程序的基本目录结构,没问题 当一个项目有多个小程序的时候,好像也没问题

  • Rust 搭建一个小程序运行环境的方法详解

    目录 从零到一:构建一个能运行小程序的App FinClip 安全沙箱的初始化 获得 SDK Key 以及 SDK Secret 的两种方式 方式一:采用 FinClip.com 托管服务 方式二:自行部署 FinClip 社区版 FinClip SDK 在 App 中的初始化 Rust 开发环境的准备 Rust 代码编译成 iOS 静态库的验证 搭建一个FinClip社区版docker运行环境,安装设置Rust开发编译iOS代码的环境,设置xcode的项目配合,集成FinClip SDK,准备

  • 利用React高阶组件实现一个面包屑导航的示例

    什么是 React 高阶组件 React 高阶组件就是以高阶函数的方式包裹需要修饰的 React 组件,并返回处理完成后的 React 组件.React 高阶组件在 React 生态中使用的非常频繁,比如react-router 中的 withRouter 以及 react-redux 中 connect 等许多 API 都是以这样的方式来实现的. 使用 React 高阶组件的好处 在工作中,我们经常会有很多功能相似,组件代码重复的页面需求,通常我们可以通过完全复制一遍代码的方式实现功能,但是这

随机推荐