一、案例

二、创建项目
| npm init vite@latest |
| # 选择react |
| # 删除不必要的css,文件等 |
| # 安装依赖classnames、sass、uuid、dayjs、lodash |
| npm i -S classnames # 处理className属性 |
| npm i -S uuid #生成uuid |
| npm i -S dayjs # 日期处理 |
| npm i -S lodash # 操作数组 |
| npm i -D sass # sass支持 |
三、目录结构

主要文件
App.jsx
| import './App.scss' |
| import avatar from './assets/bozai.png' |
| import { useRef, useState } from "react"; |
| import _ from "lodash"; |
| import classNames from "classnames"; |
| import { v4 as uuidV4 } from "uuid"; |
| import dayjs from "dayjs"; |
| /** |
| * 评论列表的渲染和操作 |
| * |
| * 1. 根据状态渲染评论列表 |
| * 2. 删除评论 |
| */ |
| // 评论列表数据 |
| const defaultList = [ |
| { |
| // 评论id |
| rpid: 3, |
| // 用户信息 |
| user: { |
| uid: '13258165', |
| avatar: 'https://paylove.online/images/sky/sky66134a508f8ac637b440b340.png', |
| uname: '周杰伦', |
| }, |
| // 评论内容 |
| content: '哎哟,不错哦', |
| // 评论时间 |
| ctime: '10-18 08:15', |
| like: 88, |
| }, |
| { |
| rpid: 2, |
| user: { |
| uid: '36080105', |
| avatar: 'https://paylove.online/images/sky/sky66134b908f8ad1ad9020fc6e.png', |
| uname: '许嵩', |
| }, |
| content: '我寻你千百度 日出到迟暮', |
| ctime: '11-13 11:29', |
| like: 88, |
| }, |
| { |
| rpid: 1, |
| user: { |
| uid: '30009257', |
| avatar, |
| uname: '小三', |
| }, |
| content: '小三报道', |
| ctime: '10-19 09:00', |
| like: 66, |
| }, |
| ] |
| // 当前登录用户信息 |
| const user = { |
| // 用户id |
| uid: '30009257', |
| // 用户头像 |
| avatar, |
| // 用户昵称 |
| uname: '小三', |
| } |
| /** |
| * 导航 Tab 的渲染和操作 |
| * |
| * 1. 渲染导航 Tab 和高亮 |
| * 2. 评论列表排序 |
| * 最热 => 喜欢数量降序 |
| * 最新 => 创建时间降序 |
| */ |
| |
| // 导航 Tab 数组 |
| const tabs = [ |
| { type: 'hot', text: '最热' }, |
| { type: 'time', text: '最新' } |
| ] |
| |
| const App = () => { |
| //评论列表 |
| const [commentList, setCommentList] = useState(_.orderBy(defaultList, 'ctime', 'desc')) |
| const handleDel = (rpid) => { |
| setCommentList(commentList.filter(item => item.rpid !== rpid)) |
| } |
| //tab切换功能 |
| // const [tabList,setTabList] = useState(tabs) |
| const [type, setType] = useState('hot') |
| |
| |
| |
| const changeTab = (type) => { |
| setType(type) |
| //基于列表的排序 |
| if (type === 'hot') { |
| //lodash |
| |
| setCommentList(_.orderBy(commentList, 'like', 'desc')) |
| } else { |
| setCommentList(_.orderBy(commentList, 'ctime', 'desc')) |
| } |
| } |
| const sort=(newList)=>{ |
| if (type === 'hot') { |
| setCommentList(_.orderBy(newList, 'like', 'desc')) |
| } else { |
| setCommentList(_.orderBy(newList, 'ctime', 'desc')) |
| } |
| } |
| |
| // 发表评论 |
| const [comment, setComment] = useState('') |
| const inputRef = useRef(null) |
| const publishComment = () => { |
| if (!comment) { |
| return |
| } |
| |
| const newComment = { |
| rpid: uuidV4(), |
| user: { |
| uid: '30009257', |
| avatar, |
| uname: '小三', |
| }, |
| content: comment, |
| ctime: dayjs(new Date()).format('MM-DD hh:mm'), |
| like: 999, |
| } |
| sort([ |
| ...commentList, |
| newComment |
| ]) |
| //clear input |
| setComment('') |
| /// |
| inputRef.current.focus() |
| |
| |
| |
| } |
| |
| return ( |
| <div className="app"> |
| {/* 导航 Tab */} |
| <div className="reply-navigation"> |
| <ul className="nav-bar"> |
| <li className="nav-title"> |
| <span className="nav-title-text">评论</span> |
| {/* 评论数量 */} |
| <span className="total-reply">{10}</span> |
| </li> |
| <li className="nav-sort"> |
| {/* 高亮类名: active */} |
| {tabs.map(tab => ( |
| // <span className={`nav-item ${type === tab.type && 'active'}`} key={tab.type} onClick={()=>changeTab(tab.type)}>{tab.text}</span> |
| <span className={classNames('nav-item', { active: type === tab.type })} key={tab.type} onClick={() => changeTab(tab.type)}>{tab.text}</span> |
| ))} |
| {/* <span className='nav-item'>最热</span> */} |
| </li> |
| </ul> |
| </div> |
| |
| <div className="reply-wrap"> |
| {/* 发表评论 */} |
| <div className="box-normal"> |
| {/* 当前用户头像 */} |
| <div className="reply-box-avatar"> |
| <div className="bili-avatar"> |
| <img className="bili-avatar-img" src={avatar} alt="用户头像" /> |
| </div> |
| </div> |
| <div className="reply-box-wrap"> |
| {/* 评论框 */} |
| <textarea |
| className="reply-box-textarea" |
| value={comment} |
| onChange={(e) => setComment(e.target.value)} |
| placeholder="发一条友善的评论" |
| ref={inputRef} |
| /> |
| {/* 发布按钮 */} |
| <div className="reply-box-send" onClick={publishComment}> |
| <div className="send-text">发布</div> |
| </div> |
| </div> |
| </div> |
| {/* 评论列表 */} |
| <div className="reply-list"> |
| {/* 评论项 */} |
| {commentList.map(item => ( |
| <div className="reply-item" key={item.rpid}> |
| {/* 头像 */} |
| <div className="root-reply-avatar"> |
| <div className="bili-avatar"> |
| <img |
| className="bili-avatar-img" |
| alt="" |
| src={item.user.avatar} |
| /> |
| </div> |
| </div> |
| |
| <div className="content-wrap"> |
| {/* 用户名 */} |
| <div className="user-info"> |
| <div className="user-name">{item.user.uname}</div> |
| </div> |
| {/* 评论内容 */} |
| <div className="root-reply"> |
| <span className="reply-content">{item.content}</span> |
| <div className="reply-info"> |
| {/* 评论时间 */} |
| <span className="reply-time">{item.ctime}</span> |
| {/* 评论数量 */} |
| <span className="reply-time">点赞数:{item.like}</span> |
| {/* user.id == item.user.id */} |
| {item.user.uid === user.uid && |
| <span className="delete-btn" onClick={() => handleDel(item.rpid)}> |
| 删除 |
| </span>} |
| |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| ) |
| } |
| |
| export default App |
| |
App.scss
| .app { |
| width: 80%; |
| margin: 50px auto; |
| } |
| .reply-navigation { |
| margin-bottom: 22px; |
| .nav-bar { |
| display: flex; |
| align-items: center; |
| margin: 0; |
| padding: 0; |
| list-style: none; |
| .nav-title { |
| display: flex; |
| align-items: center; |
| width: 114px; |
| font-size: 20px; |
| .nav-title-text { |
| color: #18191c; |
| font-weight: 500; |
| } |
| .total-reply { |
| margin: 0 36px 0 6px; |
| color: #9499a0; |
| font-weight: normal; |
| font-size: 13px; |
| } |
| } |
| .nav-sort { |
| display: flex; |
| align-items: center; |
| color: #9499a0; |
| font-size: 13px; |
| .nav-item { |
| cursor: pointer; |
| &:hover { |
| color: #00aeec; |
| } |
| &:last-child::after { |
| display: none; |
| } |
| &::after { |
| content: ' '; |
| display: inline-block; |
| height: 10px; |
| width: 1px; |
| margin: -1px 12px; |
| background-color: #9499a0; |
| } |
| } |
| .nav-item.active { |
| color: #eb1c1c; |
| } |
| } |
| } |
| } |
| |
| .reply-wrap { |
| position: relative; |
| } |
| .box-normal { |
| display: flex; |
| transition: 0.2s; |
| |
| .reply-box-avatar { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| width: 80px; |
| height: 50px; |
| } |
| |
| .reply-box-wrap { |
| display: flex; |
| position: relative; |
| flex: 1; |
| |
| .reply-box-textarea { |
| width: 100%; |
| height: 50px; |
| padding: 5px 10px; |
| box-sizing: border-box; |
| color: #181931; |
| font-family: inherit; |
| line-height: 38px; |
| background-color: #f1f2f3; |
| border: 1px solid #f1f2f3; |
| border-radius: 6px; |
| outline: none; |
| resize: none; |
| transition: 0.2s; |
| |
| &::placeholder { |
| color: #9499a0; |
| font-size: 12px; |
| } |
| &:focus { |
| height: 60px; |
| background-color: #fff; |
| border-color: #c9ccd0; |
| } |
| } |
| } |
| |
| .reply-box-send { |
| position: relative; |
| display: flex; |
| flex-basis: 86px; |
| align-items: center; |
| justify-content: center; |
| margin-left: 10px; |
| border-radius: 4px; |
| cursor: pointer; |
| transition: 0.2s; |
| |
| & .send-text { |
| position: absolute; |
| z-index: 1; |
| color: #fff; |
| font-size: 16px; |
| } |
| &::after { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| background-color: #00aeec; |
| border-radius: 4px; |
| opacity: 0.5; |
| content: ''; |
| } |
| &:hover::after { |
| opacity: 1; |
| } |
| } |
| } |
| .bili-avatar { |
| position: relative; |
| display: block; |
| width: 48px; |
| height: 48px; |
| margin: 0; |
| padding: 0; |
| border-radius: 50%; |
| } |
| .bili-avatar-img { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| display: block; |
| width: 48px; |
| height: 48px; |
| object-fit: cover; |
| border: none; |
| border-radius: 50%; |
| image-rendering: -webkit-optimize-contrast; |
| transform: translate(-50%, -50%); |
| } |
| |
| // 评论列表 |
| .reply-list { |
| margin-top: 14px; |
| } |
| .reply-item { |
| padding: 22px 0 0 80px; |
| .root-reply-avatar { |
| position: absolute; |
| left: 0; |
| display: flex; |
| justify-content: center; |
| width: 80px; |
| cursor: pointer; |
| } |
| |
| .content-wrap { |
| position: relative; |
| flex: 1; |
| |
| &::after { |
| content: ' '; |
| display: block; |
| height: 1px; |
| width: 100%; |
| margin-top: 14px; |
| background-color: #e3e5e7; |
| } |
| |
| .user-info { |
| display: flex; |
| align-items: center; |
| margin-bottom: 4px; |
| |
| .user-name { |
| height: 30px; |
| margin-right: 5px; |
| color: #61666d; |
| font-size: 13px; |
| line-height: 30px; |
| cursor: pointer; |
| } |
| } |
| |
| .root-reply { |
| position: relative; |
| padding: 2px 0; |
| color: #181931; |
| font-size: 15px; |
| line-height: 24px; |
| .reply-info { |
| position: relative; |
| display: flex; |
| align-items: center; |
| margin-top: 2px; |
| color: #9499a0; |
| font-size: 13px; |
| |
| .reply-time { |
| width: 86px; |
| margin-right: 20px; |
| } |
| .reply-like { |
| display: flex; |
| align-items: center; |
| margin-right: 19px; |
| |
| .like-icon { |
| width: 14px; |
| height: 14px; |
| margin-right: 5px; |
| color: #9499a0; |
| background-position: -153px -25px; |
| &:hover { |
| background-position: -218px -25px; |
| } |
| } |
| .like-icon.liked { |
| background-position: -154px -89px; |
| } |
| } |
| .reply-dislike { |
| display: flex; |
| align-items: center; |
| margin-right: 19px; |
| .dislike-icon { |
| width: 16px; |
| height: 16px; |
| background-position: -153px -153px; |
| &:hover { |
| background-position: -217px -153px; |
| } |
| } |
| .dislike-icon.disliked { |
| background-position: -154px -217px; |
| } |
| } |
| .delete-btn { |
| cursor: pointer; |
| &:hover { |
| color: #00aeec; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| .reply-none { |
| height: 64px; |
| margin-bottom: 80px; |
| color: #99a2aa; |
| font-size: 13px; |
| line-height: 64px; |
| text-align: center; |
| } |
| |
运行
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】