React:B站评论demo,实现列表渲染、删除按钮显示和功能实现、导航栏渲染切换及高亮显示、评论区的排序

功能要求:

1、渲染评论列表

2、删除评论功能:只显示自己评论的删除按钮;点击删除按钮,删除当前评论,列表中不再显示。

3、渲染导航Tab(最新 | 最热) 和其 高亮实现

4、评论排序功能实现(最新:按时间排序 | 最热:按点赞数排序)

核心思路:

1、使用map方法对列表数据进行遍历渲染(别忘记加key)

2、使用useState维护评论列表

3、只显示自己评论删除按钮,应当是一个条件渲染

4、删除功能:点击删除按钮时,传出唯一匹配项 id,以id为条件对评论列表做 filter过滤 。判断当前登录用户信息当中的uid 和 评论数据当中的uid 一样时,才显示这条评论的删除按钮**(user.id === item.user.id)**

5、Tab功能实现:点击谁就把谁的 type(独一无二的标识)记录下来,然后和遍历时的每一项的type 做匹配,谁匹配到就设置负责高亮的类名。

6、评论区按序排列,用到了lodash库的排序函数

源码 以及 详细代码注释 如下:

javascript 复制代码
// App.js

import './App.scss'
import jayAvatar from './images/jay.png'
import ljAvatar from './images/lj.png'
import xsAvatar from './images/xs.png'
import React, { useState } from 'react'
import _ from 'lodash'

// 评论列表的渲染和操作 1. 根据状态渲染评论列表 2. 删除评论

// 评论列表数据
const defaultList = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: jayAvatar,
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-18 08:15',
    like: 989,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: xsAvatar,
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '11-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: ljAvatar,
      uname: '李健',
    },
    content: '只是因为在人群中多看了你一眼',
    ctime: '10-19 09:00',
    like: 66,
  },
]
// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar: jayAvatar,
  // 用户昵称
  uname: '李健',
}

// 导航 Tab 的渲染和操作  1. 渲染导航 Tab 和高亮  2. 评论列表排序

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {
  // 使用useState管理评论列表,默认值为按照点赞数降序的defaultList(使用lodash库的orderBy方法)
  const [commentList, setCommentList] = useState(_.orderBy(defaultList, ['like'], ['desc']))
  // 评论删除功能
  const handleDel = (id) => {
    console.log(id)
    // 对commentList进行过滤,过滤掉id等于传入id的评论,留下id不等于传入id的评论
    setCommentList(commentList.filter(item => item.rpid !== id))
  }

  // tab切换功能
  // 1. 点击谁就把谁的唯一标识type记录下来
  // 2. 通过记录的type和每一项遍历时的type做匹配 控制激活类名的显示

  const [type, setType] = useState('hot')

  const handleTabChange = (type) => {
    console.log(type)
    setType(type)
    // 基于列表的排序
    if (type === 'hot') {
      // 最热 => 喜欢点赞数量降序
      // lodash函数库,desc降序排序
      setCommentList(_.orderBy(commentList, ['like'], ['desc']))
    } else {
      // 最新 => 创建时间降序
      // lodash函数库,desc降序排序
      setCommentList(_.orderBy(commentList, ['ctime'], ['desc']))
    }
  }


  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(item =>
              <span
                key={item.type}
                className={`nav-item ${type === item.type ? 'active' : ''}`}
                onClick = {() => handleTabChange(item.type)}>
                {item.text}
              </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={ljAvatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <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 */}
                    {user.uid === item.user.uid &&
                      <span className="delete-btn" onClick={() => handleDel(item.rpid)}>
                        删除
                      </span>
                    }

                  </div>
                </div>
              </div>
            </div>
          ) )}
          <div className="reply-item">
            {/* 头像 */}
            <div className="root-reply-avatar">
              <div className="bili-avatar">
                <img
                  className="bili-avatar-img"
                  alt=""
                />
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>
  )
}

export default App
css 复制代码
//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: #18191c;
      }
    }
  }
}

.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;
}
javascript 复制代码
// index.js

import { createRoot } from 'react-dom/client'
import App from './App'

const root = createRoot(document.querySelector('#root'))

root.render(<App />)
相关推荐
木辰風11 分钟前
vue These dependencies were not found
前端·javascript·vue.js
非晓为骁19 分钟前
【Python】在Windows下配置Python最小环境并在React执行Python脚本
windows·python·react.js
i建模22 分钟前
Tauri+React跨平台开发环境搭建全指南
前端框架·react·tauri·跨平台开发
Epicurus26 分钟前
使用vue.js插件封装粘性元素组件
前端·vue.js
前端安迪27 分钟前
Playwright中修改接口返回的5种方法
前端·单元测试
前端小胡兔31 分钟前
解决vue中formdata 传值为空 控制台报错SyntaxError - expected expression, got ‘<‘
前端·javascript·vue.js
咔咔库奇44 分钟前
【react】状态管理Context
javascript·react.js·ecmascript
uhakadotcom1 小时前
Astro 框架:高性能内容网站开发入门
前端·面试·github
Xxxxxl171 小时前
CSS - 妙用Sass
前端·css·sass
林钟雪1 小时前
基于HarmonyNext的ArkTS实战案例:构建高效的图像处理应用
前端