React仿知乎移动端想法页,超详细,小白React入门必看!

前言

React作为前端使用频率最高的三大框架之一,组件化是其核心思想。对于学习过React的前端小伙伴来说,React中一切都是组件(相信 大家都耳熟能详)。下面将介绍本人实战项目中对复杂组件的组件化设计的过程,希望对各位小伙伴有一定的帮助和启发。

项目预览

项目远程访问链接 -> 19826353321.github.io/zhihuidea/

远程项目进入不会自动跳转页面,需手动点击进行跳转。

远程访问tip.jpg

首页预览

首页上方是3个子页面,中间是子页面的信息,下方是tabbar(标签栏),分别对应4个页面。非主要页面没有做,以切页面的形式展示一下。

idea.gif

首页切详情页预览

点击首页中想法页的页面中的信息,跳转到对应信息的想法详情页,点击想法详情页左上角的返回箭头回到首页。

切页面.gif

详情页预览

想法详情页主要做了轮播图和鼓掌、收藏点击事件的交互。

ideadetail.gif

项目准备

软件工具

  • Visual Studio Code(写代码)
  • nodejs(安装依赖,执行代码)
  • fastmock(模拟后端数据接口)
  • 浏览器(负责测试调试)

项目初始化

打开vs code终端。输入

  1. npm init @vitejs/app(创建一个新的 Vite 项目)
  2. 填写项目名,连续选择2次react
  3. cd 项目名(进入创建的项目目录)
  4. npm i(安装 node_modules)
  5. npm run dev(执行前端代码)

安装依赖

package.json.png 依赖通过在终端里输入npm install 依赖名安装。

  • react-router和react-router-dom(配置路由)
  • axios(拉取后端数据)
  • styled-components(css in js,通过hash生成唯一的类名)
  • swiper(轮播图)
  • antd-mobile(组件库)
  • font-awesome(字体图标)

工程化src目录

工程化文件.png

在src目录下新建api、assets、components、config、modules、pages、routes、utils文件夹。

api目录

api目录下有request.js(负责请求数据)。

api.png

request.js中引入axios.get异步拉取后端数据。


import axios from 'axios'
export const getIdea = () => 
    axios.get(`https://www.fastmock.site/mock/ddcffb78b7166d4307e2cb22af2b93b3/idea/idea-item`)

assets目录

assets目录下有font(存下载的字体图标)和styles(存放初始化的样式文件)。字体图标可以在www.iconfont.cn/下载。

assets.png

components目录

components下存放着通用的组件。例如我的components文件里有页面首部和尾部以及其他通用组件。

components.png

modules目录

modules文件里有rem.js,负责设置根font-size来达到适配不同移动端的效果

modules.png

document.documentElement.style.fontSize =
    document.documentElement.clientWidth / 3.75 + "px"
// 横竖屏切换
window.onresize = function() {
    document.documentElement.style.fontSize =
        document.documentElement.clientWidth / 3.75 + "px"
}

pages目录

pages目录里存放着项目页面的文件。

pages.png

routes目录

routes目录下存放着网页的路由信息的jsx文件。

routes.png

utils目录

utils目录下存放判断页面是否需要首部和尾部的js文件。

utils.png

/**
 * @author  
 * @func 根据path 判断是否在数组配置中
 * @params {path string}
 * @return boolean 
 */
export const isPathPartlyExisted = (path) => {
    const arr = ['/ideadetail','/information','/search'];
    // 任何情况 结果数组第二项都是arr里匹配的单项 
    let pathRes = path.split('/')
    if (pathRes[1] && arr.indexOf(`/${pathRes[1]}`) != -1) return true
    return false
}

public\js\adapt.js

public\js\adapt.js位于项目目录之下,与src目录平级。其通过对不同大小手机屏幕设置不同的rem(根字体大小)使移动端项目具有自适应功能。

自适应.png

var init = function () {
    var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
    if (clientWidth >= 640) {
        clientWidth = 640;
    }
    var fontSize = 20 / 375 * clientWidth;
    document.documentElement.style.fontSize = fontSize + "px";
}

init();

window.addEventListener("resize", init);

项目开始

页面分析

首页分析

首页由Header组件、Footer组件和Idea组件组成。

首页结构图.jpg

详情页分析

想法详情页由头部,轮播图,文本和底部组成。 详情页结构体.jpg

main.jsx

main.jsx作为前端程序的入口,在main.jsx中引入BrowserRouter组件包裹App组件。引入字体图标样式、初始样式、swiper(实现轮播图的组件)。

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { BrowserRouter } from 'react-router-dom'
import 'font-awesome/css/font-awesome.min.css'
import './assets/font/iconfont.css'
import './assets/styles/reset.css'
import 'swiper/dist/css/swiper.min.css'
import "swiper/dist/js/swiper"

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
  )

App.jsx

在App.jsx中引入自定义组件Header(主页头部)、Footer(主页尾部)、RouteConfig组件(路由配置)构成页面。引入Suspense组件实现未加载完页面时显示在loading(加载中)。

import { useState, Suspense } from 'react'
import './App.css'
import Header from './components/Header'
import Footer from './components/Footer'
import RoutesConfig from './routes'

function App() {

  return (
    <div className="App">
      <Header />
      <Suspense fallback={<div>loading...</div>}>
        <RoutesConfig />
      </Suspense>
      <Footer />
    </div>
  )
}

export default App

RoutesConfig.jsx

    • 在RoutesConfig.jsx中引入RoutesRoute组件,用Routes组件包裹Route组件,一级路由的Route组件包裹二级路由的Route组件。
  • 引入lazy实现懒加载,将首页不用立即引入的组件进行懒加载可以提高首页的加载速度。
import { useState, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import Home from '../pages/Home'
import Idea from '../pages/Home/Idea'
const Attention = lazy(() => import('../pages/Attention'))
const Mine = lazy(() => import('../pages/Mine'))
const Vip = lazy(() => import('../pages/Vip'))
const TopSearch = lazy(() => import('../pages/Home/TopSearch'))
const Recommend = lazy(() => import('../pages/Home/Recommend'))
const IdeaDetail = lazy(() => import('../pages/Home/Idea/IdeaItem/IdeaDetail'))
const Information = lazy(() => import('../components/Information'))
const Search = lazy(() => import('../components/Search'))

const RoutesConfig = () => {
    return (
        <Routes>
            <Route path="/" element={<Home />}></Route>
            <Route path="/home" element={<Home />}>
                <Route path="/home/idea" element={<Idea />} />
                <Route path="/home/recommend" element={<Recommend />} />
                <Route path="/home/topSearch" element={<TopSearch />} />
            </Route>
            <Route path="/attention" element={<Attention />}></Route>
            <Route path="/mine" element={<Mine />}></Route>
            <Route path="/vip" element={<Vip />}></Route>
            <Route path='/ideadetail' element={<IdeaDetail />}></Route>
            <Route path='/search' element={<Search />}></Route>
            <Route path='/information' element={<Information />}></Route>
        </Routes>
    )
}

export default RoutesConfig

Header组件

Header组件作为首页的头部,主要负责导航到3个二级路由子页面。

header.png

Header组件的index.jsx

import React, { useEffect, useState } from 'react'
import { useLocation, NavLink, Link } from 'react-router-dom'
import { HeaderWrapper } from "./style";
import { isPathPartlyExisted } from '../../utils'

export default function Header() {
  const { pathname } = useLocation()
  if (isPathPartlyExisted(pathname)) return
  return (
    <HeaderWrapper>
      <div className='header'>
        <span className="header-words">
          <NavLink to={{ pathname: '/home/idea' }} className='header-word '
            style={({ isActive }) => {
              return {
                borderBottom: isActive ? "2px solid blue" : "",
                fontSize: isActive ? "0.8rem" : "0.75rem",
                fontWeight: isActive ? "700" : "400",
              }
            }}>
            想法
          </NavLink>
          <NavLink to={{ pathname: '/home/recommend' }} className='header-word '
            style={({ isActive }) => {
              return {
                borderBottom: isActive ? "2px solid blue" : "",
                fontSize: isActive ? "0.8rem" : "0.75rem",
                fontWeight: isActive ? "700" : "400",
              }
            }}>
            推荐
          </NavLink>
          <NavLink to={{ pathname: '/home/topSearch' }} className='header-word '
            style={({ isActive }) => {
              return {
                borderBottom: isActive ? "2px solid blue" : "",
                fontSize: isActive ? "0.8rem" : "0.75rem",
                fontWeight: isActive ? "700" : "400",
              }
            }}>
            热榜</NavLink>
        </span>
        <span className="header-icons">
          <Link to={{ pathname: "/search" }} className='fa fa-search '></Link>
          <Link to={{ pathname: "/information" }} className='fa fa-bell-o'></Link>
        </span>
      </div>
    </HeaderWrapper >
  )
}

使用NavLink并且设置激活状态下样式的改变。

    <NavLink to={{ pathname: '/home/idea' }} className='header-word '
            style={({ isActive }) => {
              return {
                borderBottom: isActive ? "2px solid blue" : "",
                fontSize: isActive ? "0.8rem" : "0.75rem",
                fontWeight: isActive ? "700" : "400",
              }
            }}>
            想法
          </NavLink>

Header组件的style.js

首页头部布局通过display: flex; justify-content: center;实现居中,然后将其他字体图标定位到右边。

header.png

import styled from 'styled-components'
export const HeaderWrapper = styled.div`
.header{
  position: relative;
  text-align: center;
  margin: 0.5rem 0;
  .header-words{
    display: flex;
    justify-content: center;
    .header-word{
      margin: 0 1rem;
    }
  }
  .fa{
    position: absolute;
    right:0;
    top:0;
  }
    .fa-search{
      right:2.3rem;
    }
    .fa-bell-o{
      right:0.5rem;
    }
}
a{
  font-size: 1rem;
}
`

isPathPartlyExisted(pathname)

isPathPartlyExisted(pathname)负责判断当前页面是否应该使用Header(头部)和footer(尾部)。不是所有的页面都有首页的头部和尾部,例如详情页就没有。

  1. 解释一下代码,arr数组中存放的是不需要首页头部和尾部的页面url。
  2. useLocation拿到url,将url通过split('/')分割,分割的数组中第二项为arr数组中匹配的单项。
  3. 拿这个单项和arr数组中的单项匹配,匹配成功则不加头部和尾部。
export const isPathPartlyExisted = (path) => {
    const arr = ['/ideadetail','/information','/search'];
    // 任何情况 结果数组第二项都是arr里匹配的单项 
    let pathRes = path.split('/')
    if (pathRes[1] && arr.indexOf(`/${pathRes[1]}`) != -1) return true
    return false
}
......
const { pathname } = useLocation()
  if (isPathPartlyExisted(pathname)) return

Footer组件

Footer组件位于页面尾部,称作tabbar(标签栏),可以去其他的同级功能页。

footer.png

Footer组件的index.jsx

import React from 'react'
import { Link, useLocation } from 'react-router-dom'
import { FooterWrapper } from './style'
import classnames from 'classnames'
import { isPathPartlyExisted } from '../../utils'

export default function Footer(props) {
    const { pathname } = useLocation()
    let pathRes = pathname.split('/')
    let pathname2 = "/" + pathRes[1];
    if (isPathPartlyExisted(pathname2)) return
    return (
        <FooterWrapper>
            <Link to="/home" className={classnames({ active: pathname2 == '/home' || pathname2 == '/' })}>
                <i className="fa fa-home"></i>
                <span>首页</span>
            </Link>
            <Link to="/attention" className={classnames({ active: pathname2 == '/attention' })}>
                <i className="icon-guanzhu1 iconfont"></i>
                <span>关注</span>
            </Link>
            <Link to="/vip" className={classnames({ active: pathname2 == '/vip' })}>
                <i className="iconfont icon-vip"></i>
                <span>会员</span>
            </Link>
            <Link to="/mine" className={classnames({ active: pathname2 == '/mine' })}>
                <i className="fa fa-user"></i>
                <span>我的</span>
            </Link>
        </FooterWrapper>
    )
}

Footer组件的style.js

通过position: fixed将Footer组件定位到页面的最下方。 通过display: flex;flex:1;实现平均分配空间。

footer.png

import styled from 'styled-components'

export const FooterWrapper = styled.div`
    width: 100%;
    height: 2.5rem;
    background: #fff;
    position: fixed;
    bottom: 0;
    left: 0;
    display: flex;
    a {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-around;
        font-size: 0.5rem;
        &.active {
            color: blue;
        }
        i{
            font-size: 1.25rem;
        }
    }
` 

Home组件

Home.jsx中引入Outlet组件作为二级路由出口。 Home.jsx中引入useEffectuseNavigate实现在页面第一次渲染之后重定向到网址为"/home/idea"的Idea组件。 "<></>"为页面碎片,在jsx编译成css中不会形成包裹的div。

import React, { useState, useEffect } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'

export default function Home() {
    const navigate = useNavigate()
    useEffect(() => {
        navigate(`/home/idea`)
    }, [])

    return (
        <>
            <Outlet />
        </>
    )
}

后端数据配置

后端数据用fastmock进行模拟。 后端数据接口网址:www.fastmock.site/mock/ddcffb…

import axios from 'axios'
export const getIdea = () => 
    axios.get(`https://www.fastmock.site/mock/ddcffb78b7166d4307e2cb22af2b93b3/idea/idea-item`)

后端数据.png

Idea组件

Idea.jsx中用useState定义一个状态,用useEffect在页面第一次渲染之后使用axios获取后端数据并使用setIdea将数据赋值给idea。 引入子组件(IdeaItem)负责Idea页面的数据渲染。

import React from 'react'
import { useEffect, useState } from 'react'
import { getIdea } from '@/api/request';
import IdeaItem from "./IdeaItem";

export default function Idea() {
  const [idea, setIdea] = useState([])
  useEffect(() => {
    (async () => {
      let { data } = await getIdea()
      console.log(data)
      setIdea(data)
    })()
  }, [])

  return (
   <IdeaItem idea={idea} />           
  )
}

IdeaItem组件

IdeaItem组件的index.jsx

import React, { useEffect } from 'react'
import { Wrapper } from "./style";
import { Link } from "react-router-dom";
export default function IdeaItem({ idea }) {

  return <Wrapper >
    <div className=' idea-item'>
      <ul>
        {idea.map(item => (
          <li key={item.id} >
            <Link to={{
              pathname: '/ideadetail',
              search: `id=${item.id}`
            }} >
              <img className='img' src={item.imgs[0].img} alt="" />
              <div className='idea-body'>
                <p>{item.content}</p>
                <div className='ideaitem-footer flex'>
                  <span className='flex'>
                    <img src={item.userimg} alt="" className='user-img' />
                    <span className='username'>{item.username}</span>
                  </span>
                  <span>
                    <i className='iconfont icon-guzhang'></i>
                    <span className='guzhangnumber'>{item.guzhangnumber ? item.guzhangnumber : ""}</span>
                  </span>
                </div>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  </Wrapper >
  
 IdeaItem.propTypes = {
     idea:propTypes.array.isRequired
}
}

页面通过map函数将数据数组从js转变为jsx以实现页面元素的生成。对应代码如下:

{idea.map(item => (
          <li key={item.id} >
          ......
          </li>
        ))}

将从父组件Idea中获取的idea数据通过propTypes进行校验,该处设置收到的数据必须是一个数组,否则调试时浏览器会显示警告。对应代码如下:

IdeaItem.propTypes = {
     idea:propTypes.array.isRequired
}

使用Link实现跳转到对应详情页。to属性有2个参数,pathname表示跳转的相对路径,search表示url后携带如“?id=1”形式的数据,该参数可在跳转后的页面中获取,以达到跳转到对应点击页面的详情页。对应代码如下:

<Link to={{
              pathname: '/ideadetail',
              search: `id=${item.id}`
            }} >
            ...... </Link>

IdeaItem组件的style.js

通过ul>li并且设置好li的宽度(我使用的是rem适配方案)实现一行放2个和float: left;实现布局。

idea布局.png

import styled from 'styled-components'
export const Wrapper = styled.div`
.idea-item{
    background-color: #f6f6f6;
    padding-left: 0.5rem;     
    ul {          
        height: 50rem;     
        li{
            float: left;
            width:8.6rem;          
            margin-right: 0.5rem;
            margin-bottom: 0.5rem;
            background-color: #fff;
            border-radius: 0.1rem;
            .img{
                width: 100%;
                margin-bottom: 0.1rem;               
            }
            .idea-body{
                padding: 0 0.7rem;
                p{      
                    display: -webkit-box;
                    -webkit-line-clamp:2;
                    -webkit-box-orient:vertical;
                    -webkit-box-orient: vertical;
                    overflow: hidden;
                    text-overflow:ellipsis;
                    margin:0.7rem 0;  
                    font-size: 0.65rem;
                }    
                .ideaitem-footer{
                    font-size: 0.16rem;
                    justify-content: space-between;
                    .user-img{
                        width: 0.8rem;
                        height:0.8rem;
                        margin-right:0.3rem;
                        border-radius:50%;
                    }
                    i{
                        margin-right: 0.05rem;
                    }
                    .guzhangnumber{
                        margin-right: 0.1rem;
                    }
                }                   
            }
        }
    }
}
.flex{
    display: flex;
}
`

IdeaDetail组件

IdeaDetail组件的.jsx

import React, { useEffect, useState } from 'react'
import { useNavigate, Outlet, useParams, Link } from 'react-router-dom'
import { getIdea } from "@/api/request";
import { IdeaDetailWrapper } from "./style";
import Swiper from 'swiper'
import IdeaDetailFooter from "./IdeaDetailFooter";

export default function IdeaDetail() {
  const result = new URLSearchParams(location.search);
  const id = result.get('id');
  const [idea, setIdea] = useState([])
  const [imgData, setImgData] = useState([])
  useEffect(() => {
    (async () => {
      let { data } = await getIdea()
      data = data.filter((item) => item.id == id)
      setIdea(data)
      const { imgs } = data[0];
      setImgData(imgs)
    })()
  }, [])
  useEffect(() => {
    let swiper;
    if (!swiper) {
      swiper = new Swiper('.swiper-container', {
        loop: true,
        pagination: {
          el: '.swiper-pagination',
        }
      })
    }
  }, [imgData])
  return (
    <IdeaDetailWrapper>
      {idea.map((item) =>
        <div key={item.id} >
          <div className='idea-detail-header'>
            <span className="idea-detail-header-left">
              <Link to={{
                pathname: '/home/idea',
              }} >
                <i className='fa fa-angle-left'></i>
              </Link>
              <img src={item.userimg} alt="userimg" className='userimg' />
              <span className='username'>{item.username}</span>
            </span>
            <span className="idea-detail-header-right">
              <p className='concern'>+ 关注</p>
              <i className='fa-ellipsis-v fa'></i>
            </span>
          </div>
          <div className="swiper-container img-swiper">
            <div className="swiper-wrapper">
              {imgData.map((imgs) => (<div className="swiper-slide" key={imgs.imgId}>
                <img src={imgs.img} width="100%" />
              </div>))}
            </div>
            <div className="swiper-pagination"></div>
          </div>
          <p className='content'>{item.content}</p>
        </div>
      )}
      <IdeaDetailFooter idea={idea} />
    </IdeaDetailWrapper>

  )
}

IdeaDetail组件中获取url中传过来的id。

import { useNavigate } from 'react-router-dom'
......
const result = new URLSearchParams(location.search);
  const id = result.get('id');

拉取后端数据并处理。

const [idea, setIdea] = useState([])
  const [imgData, setImgData] = useState([])
  useEffect(() => {
    (async () => {
      let { data } = await getIdea()
      data = data.filter((item) => item.id == id)
      setIdea(data)
      const { imgs } = data[0];
      setImgData(imgs)
    })()
  }, [])

swiper

  1. 使用swiper可以实现图片轮播效果。
  2. 使用swiper需要一些准备工作。首先在nodejs中执行 npm install [email protected](@4.5.1表示指定版本)安装swiper依赖,然后在main.jsx中引入import 'swiper/dist/css/swiper.min.css'和在使用swiper的jsx文件里引入import Swiper from 'swiper'
  3. 使用useEffect或if只实例化一次Swiper,并配置swiper的生效位置.swiper-container所指的盒子中。
  • loop设置为 true 则开启循环(loop)模式(loop模式:会在原本slide 前后复制若干个slide (默认一个)并在合适的时候切换,让Swiper看起来像是循环的。)
  • pagination使用分页器导航。分页器可使用小圆点样式(默认)、分式样式或进度条样式。参数el对应的是挂载点,对应圆点所在的盒子。
  1. 一般swiper的使用格式是swiper-container包含swiper-wrapper和swiper-pagination,swiper-wrapper又包含多个swiper-slide(内置要轮播的图片)。
  2. 大家对swiper感兴趣的可以去看看文档。swiper中文->www.swiper.com.cn/

//main.js
import 'swiper/dist/css/swiper.min.css'

//index.js
import Swiper from 'swiper'
......
useEffect(() => {
    let swiper;
    if (!swiper) {
      swiper = new Swiper('.swiper-container', {
        loop: true,
        pagination: {
          el: '.swiper-pagination',
        }
      })
    }
  }, [imgData])
  ......
     <div className="swiper-container img-swiper">
            <div className="swiper-wrapper">
              {imgData.map((imgs) => (<div className="swiper-slide" key={imgs.imgId}>
                <img src={imgs.img} width="100%" />
              </div>))}
            </div>
            <div className="swiper-pagination"></div>
          </div>
  

IdeaDetail组件的style.js

样式文件通过css in js的形式编写,具有可嵌套、便于使用变量等优点。styled-components可以通过hash生成独一无二的样式名。


import styled from 'styled-components'
export const IdeaDetailWrapper = styled.div`
.idea-detail-header{
    padding-top: 0.1rem;
    display:flex;
    justify-content: space-between;
    .idea-detail-header-left,.idea-detail-header-right{
        display:flex;
        align-items: center;  
        .fa-angle-left{
            font-size: 1.5rem;
            margin: 0 1rem;
        }
        .userimg{
            width: 1.5rem;
            height: 1.5rem;
            border-radius: 50%;
            margin-right: 0.5rem;
        }
        .username{
            font-size:0.5rem;
            font-weight: 700;
        }
    }
    .idea-detail-header-right{
        .concern{
            color:blue;
            font-weight:700;
        }    
        .fa-ellipsis-v{ 
            font-size: 1rem;
            margin: 0 1rem;
        }
    }   
}
.swiper-img{
    width: 100%;
}
.content{
    font-size: 0.7rem;
    margin: 1.5rem 0;
    padding: 0 1rem;
}
`

头部布局通过flex布局的justify-content属性的space-between(将flex盒子的直接子元素优先从两边开始布局)。然后再修改元素的margin值调间距就实现了两端布局。

.idea-detail-header{
    display:flex;
    justify-content: space-between;
}

ideadetail-header.png

IdeaDetailFooter组件

IdeaDetailFooter组件实现的是想法详情页的尾部。

ideadetail-footer.png

IdeaDetailFooter组件的index.jsx

import React, { useEffect, useState } from 'react'
import { Wrapper } from './style';
export default function IdeaDetailFooter({ idea }) {
    const [guzhangNumber, setGuzhangNumber] = useState(0)
    const [isGuzhang, setIsGuzhang] = useState(false)
    const [shoucangNumber, setShoucangNumber] = useState(0)
    const [isShoucang, setIsShoucang] = useState(false)
    const [shareNumber, setShareNumber] = useState(0)
    const [isShare, setIsShare] = useState(false)
    const [commentNumber, setCommentNumber] = useState(0)
    const [isComment, setIsComment] = useState(false)
    useEffect(() => {
        if (idea[0]) {
            let data = idea[0]
            let { guzhangnumber, shoucangnumber, sharenumber, commentnumber } = data
            setGuzhangNumber(guzhangnumber)
            setShoucangNumber(shoucangnumber)
            setShareNumber(sharenumber)
            setCommentNumber(commentnumber)
        }
    }, [idea])
    const ChangeGuzhangNumber = () => {
        console.log(isGuzhang);
        if (!isGuzhang) {
            let num = guzhangNumber
            setGuzhangNumber(num + 1)
            setIsGuzhang(true)
        }
        if (isGuzhang) {
            let num = guzhangNumber
            setGuzhangNumber(num - 1)
            setIsGuzhang(false)
        }
    }
    const ChangeShoucangNumber = () => {
        console.log(isShoucang);
        if (!isShoucang) {
            let num = shoucangNumber
            setShoucangNumber(num + 1)
            setIsShoucang(true)
        }
        if (isShoucang) {
            let num = shoucangNumber

            setShoucangNumber(num - 1)
            setIsShoucang(false)
        }
    }

    return (
        <Wrapper>
            <div className="footer">
                <div className="comment">说点什么~</div>
                <div className="font-items">
                    <div className="font-item">
                        {isGuzhang ? <i className="iconfont icon-guzhang active-blue" onClick={ChangeGuzhangNumber}></i>
                            : <i className="iconfont icon-guzhang" onClick={ChangeGuzhangNumber}></i>}
                        <div className="font-number" >{guzhangNumber ? guzhangNumber : "鼓掌"}
                        </div>
                    </div>
                    <div className="font-item">
                        {isShoucang ? <i className="iconfont icon-shoucang active-blue" onClick={ChangeShoucangNumber}></i>
                            : <i className="iconfont icon-shoucang" onClick={ChangeShoucangNumber}></i>}
                        <div className="font-number" >{shoucangNumber ? shoucangNumber : "收藏"}
                        </div>
                    </div>
                    <div className="font-item">
                        <i className="iconfont icon-loop-full"></i>
                        <div className="font-number">{shareNumber ? shareNumber : "转发"}</div>
                    </div>
                    <div className="font-item">
                        <i className="iconfont icon-pinglun"></i>
                        <div className="font-number">{commentNumber ? commentNumber : "评论"}</div>
                    </div>
                </div>
            </div>
        </Wrapper>
    )
}

击掌效果实现

通过设置状态true或false记录鼓掌按钮的激活和未激活,通过if判断分别对鼓掌数进行加减。并且点击后增加新的类名(效果为修改字体图标的颜色),实现击掌按钮的效果。后期连接数据库后应将修改后的数据写回数据库。

//index.jsx
    const [guzhangNumber, setGuzhangNumber] = useState(0)
    const [isGuzhang, setIsGuzhang] = useState(false)
    ......
     const ChangeGuzhangNumber = () => {
     if (!isGuzhang) {
         let num = guzhangNumber
         setGuzhangNumber(num + 1)
         setIsGuzhang(true)
     }
        if (isGuzhang) {
         let num = guzhangNumber
         setGuzhangNumber(num - 1)
         setIsGuzhang(false)
     }
     }
     ......
{isGuzhang 
? <i className="iconfont icon-guzhang active-blue" onClick={ChangeGuzhangNumber}></i>
: <i className="iconfont icon-guzhang" onClick={ChangeGuzhangNumber}></i>}
<div className="font-number" >{guzhangNumber ? guzhangNumber : "鼓掌"}
                        </div>
                        
     //style.js
      .active-blue{
      color:#0066ff;
   }

IdeaDetailFooter组件的style.js

通过position: fixed将想法详情页尾部定位到页面的最下方。 通过display: flex;flex:1;实现自适应分配空间。评论框没有设置长度,通过flex:1占据了剩余的空间。

import styled from 'styled-components'
export const Wrapper = styled.div`
.footer{
    width: 100%;
    height: 2.5rem;
    background: #fff;
    position: fixed;
    bottom: 0;
    left: 0;
    margin-top:0.1rem;
    display: flex;
    border-top: 1px solid black;
    padding: 0.1rem 0;  
    .comment{      
        background-color: #f6f6f6;
        margin:0.25rem 0.25rem;
        font-size: 0.7rem;
        flex:1;
        padding-left: 1rem;
        height: 2rem;
        line-height: 2rem;
        border-radius:1rem;       
    }
    .font-items{
        display: flex;
    .font-item{
        padding: 0 0.75rem;
        i{
            font-size: 1.2rem;
        }
        .active-blue{
            color:#0066ff;
        }
        .font-number{
            font-size: 0.1rem;
        }
    }   
    }
}
`

项目源码地址 github -> github.com/19826353321…

猜你喜欢

转载自juejin.im/post/7116172205884456991
今日推荐