前言
React作为前端使用频率最高的三大框架之一,组件化是其核心思想。对于学习过React的前端小伙伴来说,React中一切都是组件(相信 大家都耳熟能详)。下面将介绍本人实战项目中对复杂组件的组件化设计的过程,希望对各位小伙伴有一定的帮助和启发。
项目预览
项目远程访问链接 -> 19826353321.github.io/zhihuidea/
远程项目进入不会自动跳转页面,需手动点击进行跳转。
首页预览
首页上方是3个子页面,中间是子页面的信息,下方是tabbar(标签栏),分别对应4个页面。非主要页面没有做,以切页面的形式展示一下。
首页切详情页预览
点击首页中想法页的页面中的信息,跳转到对应信息的想法详情页,点击想法详情页左上角的返回箭头回到首页。
详情页预览
想法详情页主要做了轮播图和鼓掌、收藏点击事件的交互。
项目准备
软件工具
- Visual Studio Code(写代码)
- nodejs(安装依赖,执行代码)
- fastmock(模拟后端数据接口)
- 浏览器(负责测试调试)
项目初始化
打开vs code终端。输入
npm init @vitejs/app
(创建一个新的 Vite 项目)- 填写项目名,连续选择2次react
- cd 项目名(进入创建的项目目录)
- npm i(安装 node_modules)
- npm run dev(执行前端代码)
安装依赖
依赖通过在终端里输入
npm install 依赖名
安装。
- react-router和react-router-dom(配置路由)
- axios(拉取后端数据)
- styled-components(css in js,通过hash生成唯一的类名)
- swiper(轮播图)
- antd-mobile(组件库)
- font-awesome(字体图标)
工程化src目录
在src目录下新建api、assets、components、config、modules、pages、routes、utils文件夹。
api目录
api目录下有request.js(负责请求数据)。
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/下载。
components目录
components下存放着通用的组件。例如我的components文件里有页面首部和尾部以及其他通用组件。
modules目录
modules文件里有rem.js,负责设置根font-size来达到适配不同移动端的效果
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目录里存放着项目页面的文件。
routes目录
routes目录下存放着网页的路由信息的jsx文件。
utils目录
utils目录下存放判断页面是否需要首部和尾部的js文件。
/**
* @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(根字体大小)使移动端项目具有自适应功能。
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组件组成。
详情页分析
想法详情页由头部,轮播图,文本和底部组成。
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中引入
Routes
和Route
组件,用Routes
组件包裹Route
组件,一级路由的Route
组件包裹二级路由的Route
组件。
- 在RoutesConfig.jsx中引入
- 引入
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组件的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;
实现居中,然后将其他字体图标定位到右边。
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(尾部)。不是所有的页面都有首页的头部和尾部,例如详情页就没有。
- 解释一下代码,
arr
数组中存放的是不需要首页头部和尾部的页面url。 - 用
useLocation
拿到url,将url通过split('/')
分割,分割的数组中第二项为arr数组中匹配的单项。 - 拿这个单项和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组件的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;
实现平均分配空间。
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中引入useEffect
和useNavigate
实现在页面第一次渲染之后重定向到网址为"/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`)
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;
实现布局。
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
- 使用swiper可以实现图片轮播效果。
- 使用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'
。 - 使用useEffect或if只实例化一次Swiper,并配置swiper的生效位置
.swiper-container
所指的盒子中。
- loop设置为 true 则开启循环(loop)模式(loop模式:会在原本slide 前后复制若干个slide (默认一个)并在合适的时候切换,让Swiper看起来像是循环的。)
- pagination使用分页器导航。分页器可使用小圆点样式(默认)、分式样式或进度条样式。参数el对应的是挂载点,对应圆点所在的盒子。
- 一般swiper的使用格式是swiper-container包含swiper-wrapper和swiper-pagination,swiper-wrapper又包含多个swiper-slide(内置要轮播的图片)。
- 大家对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;
}
IdeaDetailFooter组件
IdeaDetailFooter组件实现的是想法详情页的尾部。
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;
}
}
}
}
`