React 《入门案例》

一、案例

image

二、创建项目

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支持

三、目录结构

image

主要文件

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

运行

npm run dev
posted @ 2024-04-26 15:44  一个小笨蛋  阅读(10)  评论(0编辑  收藏  举报