溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么利用React實(shí)現(xiàn)一個(gè)電梯小程序

發(fā)布時(shí)間:2022-08-27 09:24:48 來源:億速云 閱讀:151 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“怎么利用React實(shí)現(xiàn)一個(gè)電梯小程序”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    查看效果

    我們先來看一下今天要實(shí)現(xiàn)的示例的效果,如下所示

    怎么利用React實(shí)現(xiàn)一個(gè)電梯小程序

    好,接下來我們也看到了這個(gè)示例的效果,讓我們進(jìn)入正題,開始愉快的編碼吧。

    技術(shù)棧介紹

    這個(gè)小程序,我們將采用React + typescript + css in js語法編寫,并且采用最新比較流行的工具vite來構(gòu)建。

    初始化項(xiàng)目

    我們可以選擇在電腦按住shift,然后右鍵,選擇powershell,也就是默認(rèn)的系統(tǒng)終端。然后輸入命令:

    mkdir react-elevator

    創(chuàng)建一個(gè)目錄,創(chuàng)建好之后,接著我們在vscode中打開這個(gè)目錄,打開之后,在vscode中打開終端,輸入以下命令:

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

    注意在命令界面,我們要選擇react,react-ts。初始化項(xiàng)目好了之后,我們在輸入命令:

    cd react-elevator
    npm install
    npm run dev

    查看一下我們初始化項(xiàng)目是否成功。

    特別聲明: 請注意安裝了node.js和npm工具

    css in js

    可以看到,我們的項(xiàng)目初始化已經(jīng)完成,好,接下來,我們還要額外的裝一些項(xiàng)目當(dāng)中遇到的依賴,例如css in js,我們需要安裝@emotion/styled,@emotion/react依賴。繼續(xù)輸入命令:

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

    安裝好之后,我們在項(xiàng)目里面使用一下該語法。

    首先引入styled,如下:

    import styled from "@emotion/styled"

    接著創(chuàng)建一個(gè)樣式組件,css in js實(shí)際上就是把每個(gè)組件當(dāng)成一個(gè)樣式組件,我們可以通過styled后面跟html標(biāo)簽名,然后再跟模板字符串,結(jié)構(gòu)如下:

    const <組件名> = styled.<html標(biāo)簽名>`
        //這里寫樣式代碼
    `

    例如:

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

    以上代碼就是寫一個(gè)字體顏色為白色的超鏈接組件,然后我們就可以在jsx當(dāng)中直接寫link組件。如下所示:

    <div>
        <Link>這是一個(gè)超鏈接組件</Link>
    </div>

    當(dāng)然emotion還支持對象寫法,但是我們這里基本上只用模板字符串語法就夠了。

    接下來步入正題,我們首先刪除初始化的一些代碼,因?yàn)槲覀儧]有必要用到。

    分析程序的結(jié)構(gòu)

    好刪除之后,我們接下來看一下我們要實(shí)現(xiàn)的電梯小程序的結(jié)構(gòu):

    1.電梯井(也就是電梯上升或者下降的地方)

    2.電梯

    3.電梯門(分為左右門)

    4.樓層

    • 4.1 樓層數(shù)

    • 4.2 樓層按鈕(包含上升和下降按鈕)

    結(jié)構(gòu)好了之后,接下來我們來看看有哪些功能:

    • 點(diǎn)擊樓層,催動(dòng)電梯上升或者下降

    • 電梯到達(dá)對應(yīng)樓層,電梯左右門打開

    • 門打開之后,里面的美女就出來啦

    • 按鈕會(huì)有一個(gè)點(diǎn)擊選中的效果

    我們先來分析結(jié)構(gòu),根據(jù)以上的拆分,我們可以大致將整個(gè)小程序分成如下幾個(gè)組件:

    1.樓房(容器組件)

    2.電梯井組件

    2.1 電梯組件

    2.1.1 電梯左邊的門

    2.1.1 電梯右邊的門

    3.樓層組件

    3.1 樓層控制組件

    3.1.1 樓層上升按鈕組件

    3.1.2 樓層下降按鈕組件

    3.2 樓層數(shù)組件

    我們先來寫好組件和樣式,然后再完成功能。

    樓房組件

    首先是我們的樓房組件,我們新建一個(gè)components目錄,再新建一個(gè)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

    這樣,我們的一個(gè)樓房組件就算是完成了,然后我們在App.tsx當(dāng)中引入,并使用它:

    //這里是新增的代碼
    import ElevatorBuild from "./components/ElevatorBuild"
    
    const App = () => (
      <div className="App">
        {/*這里是新增的代碼 */}
        <ElevatorBuild />
      </div>
    )
    
    export default App

    全局樣式

    在這里,我們定義了全局css變量樣式,因此在當(dāng)前目錄下創(chuàng)建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;
    }

    電梯井組件

    接下來,讓我們繼續(xù)完成電梯井組件,同樣在components目錄下新建一個(gè)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

    電梯門組件

    接著我們來完成電梯門組件,我們可以看到電梯門組件有一些公共的樣式部分,所以我們可以抽取出來,新建一個(gè)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 }

    由于我們功能會(huì)需要設(shè)置這兩個(gè)組件的樣式,并且我們這個(gè)樣式是設(shè)置在style屬性上的,因此我們可以通過props來傳遞,現(xiàn)在我們先寫好typescript接口類,創(chuàng)建一個(gè)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

    接下來,我們來看兩個(gè)電梯門組件,首先是左邊的門,如下所示:

    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是一個(gè)泛型,傳入接口,代表將接口的每個(gè)屬性變成可選屬性,根據(jù)這個(gè)原理,我們可以得知右邊門的組件代碼也很類似。如下:

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

    這兩個(gè)組件寫好之后,我們接下來要在電梯組件里引入并使用它們,由于功能邏輯會(huì)需要設(shè)置樣式,因此,我們通過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

    完成了電梯組件之后,接下來我們在電梯井組件里面引入電梯組件,注意這里后續(xù)邏輯我們會(huì)設(shè)置電梯組件和電梯門組件的樣式,因此在電梯井組件中,我們需要通過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

    電梯門組件的開啟動(dòng)畫

    我們可以看到,當(dāng)?shù)竭_(dá)一定時(shí)間,電梯門會(huì)有開啟動(dòng)畫,這里我們顯然沒有加上,所以我們可以為電梯門各自加一個(gè)是否開啟的props用來傳遞,繼續(xù)修改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語法可以通過函數(shù)來返回一個(gè)css屬性,從而達(dá)到動(dòng)態(tài)設(shè)置屬性的目的,一對尖括號,其實(shí)也就是typescript中的泛型,代表是否傳入toggle數(shù)據(jù),接下來修改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;

    但是別忘了我們這里的電梯組件因?yàn)樾枰仙拖陆?,因此還需要設(shè)置樣式,再次修改一下電梯組件的代碼如下:

    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

    樓層容器組件

    到目前為止,我們的左半邊部分已經(jīng)完成了,接下來,我們來完成右半邊部分的樓層數(shù)和控制按鈕組件,我們的樓層是動(dòng)態(tài)生成的,因此我們需要一個(gè)容器組件包裹起來,先寫這個(gè)樓層容器組件,如下所示:

    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))
        // 也許這不是一個(gè)好的方法
        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;

    可以看到樓層組件的邏輯非常多,但其實(shí)一項(xiàng)一項(xiàng)的分析下來也并不難。

    接下來,我們在該容器組件中引入,并且將該組件在樓房組件中引入,就可以得到我們整個(gè)電梯小程序的結(jié)構(gòu)了。

    在這里我們來一步一步的分析樓層組件的邏輯,

    樓層數(shù)

    首先樓層是動(dòng)態(tài)生成的,通過父組件傳遞,因此我們在props當(dāng)中定義一個(gè)count,默認(rèn)值是6,代表默認(rèn)生成的樓層數(shù)。這也就是我們這行代碼的意義:

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

    樓層的上升與下降

    其次我們在對電梯進(jìn)行上升和下降的時(shí)候,需要獲取到每一層樓高,實(shí)際上也就是樓層容器元素的高度,如何獲取DOM元素的實(shí)際高度?我們先想一下,如果是一個(gè)真實(shí)的DOM元素,我們只需要獲取offsetHeight就行了,即:

    //這里的el顯然是一個(gè)dom元素
    const offset: number = el.offsetHeight;

    在react中,我們應(yīng)該如何獲取真實(shí)的DOM元素呢?利用ref屬性,首先導(dǎo)入createRef方法,創(chuàng)建一個(gè)storeyRef,然后將該storeyRef綁定到組件容器元素上,即:

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

    然后我們就可以使用useEffect方法,也就是react hooks中的一個(gè)生命周期鉤子函數(shù),監(jiān)聽這個(gè)storeyRef,如果監(jiān)聽到了,就可以直接拿到dom元素,并且使用一個(gè)狀態(tài)來存儲(chǔ)高度值。即:

    const [offset,setOffset] = useState(0)
    //...
    useEffect(() => {
        //storeyRef.current顯然就是我們實(shí)際拿到的DOM元素
        if(storeyRef){
          setOffset(storeyRef.current?.offsetHeight as number)
        }
    },[storeyRef])

    樓層列表渲染

    接下來,我們來看樓層數(shù)的動(dòng)態(tài)生成,我們知道在react中動(dòng)態(tài)生成列表元素,實(shí)際上就是使用數(shù)組的map方法,因此我們要根據(jù)count來生成一個(gè)數(shù)組,在這里,我們可以生成一個(gè)key數(shù)組,但是由于我們要控制按鈕的禁用,因此額外添加一個(gè)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);
    });

    這里涉及到一個(gè)模擬useComponentDidMount鉤子函數(shù),很簡單,在hooks目錄下新建一個(gè)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的一種寫法,可以理解它就是一個(gè)占位標(biāo)簽,沒有什么實(shí)際含義,storeyList默認(rèn)值是undefined,因此需要加?代表可選鏈,這里包含了兩個(gè)部分,第一個(gè)部分就是控制按鈕,第二部分就是顯示樓層數(shù)。

    實(shí)際上我們生成的元素?cái)?shù)組中的key就是樓層數(shù),這也是這行代碼的意義:

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

    還有就是react在生成列表的時(shí)候,需要綁定一個(gè)key屬性,方便虛擬DOM,diff算法的計(jì)算,這里不用多講。接下來我們來看按鈕組件的邏輯。

    樓層按鈕組件

    按鈕組件的邏輯包含三個(gè)部分:

    • 禁用效果

    • 點(diǎn)擊使得電梯上升和下降

    • 選中效果

    我們知道最高樓的上升是無法上升的,所以需要禁用,同樣的,底樓的下降也是需要禁用的,所以這兩行代碼就是這個(gè)意思:

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

    接下來還有一個(gè)條件,這個(gè)item.disabled其實(shí)主要是防止重復(fù)點(diǎn)擊的問題,當(dāng)然這并不是一個(gè)好的解決辦法,但目前來說我們先這樣做,定義一個(gè)type狀態(tài),代表是點(diǎn)擊的上升還是下降:

    //接口類型,type應(yīng)只能是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>();

    然后定義一個(gè)checked狀態(tài),代表當(dāng)前按鈕是否選中:

    //checked存儲(chǔ)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'

    我們樣式當(dāng)中是添加了checked的,這個(gè)沒什么好說的。

    然后,我們還需要緩存當(dāng)前樓層是哪一樓,因?yàn)橄麓吸c(diǎn)擊的時(shí)候,我們就需要根據(jù)當(dāng)前樓層來計(jì)算,而不是重頭開始。

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

    最后,就是我們的點(diǎn)擊上升和下降邏輯,還是有點(diǎn)復(fù)雜的:

    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))
        // 也許這不是一個(gè)好的方法
        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);
    };

    該函數(shù)有三個(gè)參數(shù),第一個(gè)代表當(dāng)前樓層的key值,也就是樓層數(shù),第二個(gè)代表當(dāng)前樓的索引,注意索引和樓層數(shù)是不一樣的,第三個(gè)就是點(diǎn)擊的是上升還是下降。我們的第一個(gè)參數(shù)和第三個(gè)參數(shù)是用來設(shè)置按鈕的選中效果,即:

    setChecked(key)
    setType(method)

    接下來,我們需要計(jì)算動(dòng)畫的執(zhí)行時(shí)間,例如我們從第一層到第五層,如果按每秒到一層來計(jì)算,那么第一層到第五層就需要4s的時(shí)間,同理我們的偏移量就應(yīng)該是每層樓高與需要移動(dòng)的樓高在減去1。因此,計(jì)算需要移動(dòng)的樓高我們是:

    const moveFloor = count - index
    const diffFloor = Math.abs(moveFloor - currentFloor)
    //設(shè)置當(dāng)前樓層
    setCurrentFloor(moveFloor) 
    props[method]?.(diffFloor, offset * (moveFloor - 1))

    注意我們是將事件拋給父組件的,因?yàn)槲覀兊碾娞萁M件和樓層容器組件在同一層級,只有這樣,才能設(shè)置電梯組件的樣式。即:

    //傳入兩個(gè)參數(shù),代表動(dòng)畫的執(zhí)行時(shí)間和偏移量,props[method]其實(shí)就是動(dòng)態(tài)獲取props中的屬性
    props[method]?.(diffFloor, offset * (moveFloor - 1))

    然后這里的邏輯就是防止重復(fù)點(diǎn)擊的代碼,這并不是一個(gè)好的解決方式:

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

    修改樓層容器組件

    好了,這個(gè)組件的分析就到此為止了,我們既然把事件拋給了父組件,因此我們還需要修改一下它的父組件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;

    最后,我們來檢查一下代碼,看看還有沒有什么可以優(yōu)化的地方,可以看到我們的按鈕禁用邏輯是可以復(fù)用的,我們再重新創(chuàng)建一個(gè)函數(shù),即:

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

    到此為止,我們的一個(gè)電梯小程序就算是完成了,也不算是復(fù)雜,總結(jié)一下我們學(xué)到的知識(shí)點(diǎn):

    • css in js 我們使用的是@emotion這個(gè)庫

    • 父子組件的通信,使用props

    • 操作DOM,使用ref

    • 組件內(nèi)的狀態(tài)通信,使用useState,以及如何修改狀態(tài),有兩種方式

    • 鉤子函數(shù)useEffect

    • 類名的操作與事件還有就是樣式的設(shè)置

    • React列表渲染

    • typescript接口的定義,以及一些內(nèi)置的類型

    “怎么利用React實(shí)現(xiàn)一個(gè)電梯小程序”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI