首页 前端 正文

React组件设计,仿米游社首页频道设置页面

   前言    作为一个刚接触react 组件设计不久的新人,独立完成一个组件的设计开发其中过程是十分卡手的,本篇详尽的描述了米游社首页频道选择页面组件开发的全过程,希望这个这个简单组件的设计开发能对和我一样接触react组件开发不久的人有点帮助    准备阶段    页面分析    在正式开始仿页面之前,先

前言

作为一个刚接触react 组件设计不久的新人,独立完成一个组件的设计开发其中过程是十分卡手的,本篇详尽的描述了米游社首页频道选择页面组件开发的全过程,希望这个这个简单组件的设计开发能对和我一样接触react组件开发不久的人有点帮助

准备阶段

页面分析

在正式开始仿页面之前,先看下原页面效果:

React组件设计,仿米游社首页频道设置页面  第1张 布局十分常见,头、身、尾,三部分,对应三个组件,点击推荐频道组件中的添加符号,可以添加到我的频道组件中,我的频道中的列表数据可以长按进行拖拽排序,原页面那个句柄符号好像就是提示用,没有实际功能作用,整个列表长按都可以拖拽,当删除到最后一个游戏时会有一个小的模态框提示,原页面数据发生改变右上角确定高亮,综上我们需要完成:

  • 监听列表数据state 改变实现增加删除

  • 我的频道列表长按拖拽排序

  • 我的频道列表只剩一个游戏时,删除弹出提示

  • 数据发生改变,tab 中确定按钮高亮显示 根据需求我划分组件文件目录如下:      

    SelectChannel
    ├─ Body
    │  ├─ content
    │  │  ├─ index.jsx
    │  │  └─ style.js
    │  ├─ index.jsx
    │  └─ style.js
    ├─ Footer
    │  ├─ content
    │  │  ├─ index.jsx
    │  │  └─ style.js
    │  ├─ index.jsx
    │  └─ style.js
    ├─ Header
    │  ├─ index.jsx
    │  └─ style.js
    ├─ index.jsx
    └─ style.js

    使用工具

    vite: 脚手架,初始化react项目
    dnd-kit: 拖拽排序功能就是靠他实现的,官方文档
    styled-components: css in js,官方文档
    classnames: 动态类名,官方文档
    fastmock: 接口假数据
    axios: 数据请求

开发阶段

1. 初始化项目

  • 终端npm init @vitejs/app 对项目进行初始化工作,根据提示输入项目名,选react,顺便打开生成的vite配置文件设置src目录别名为@

  • fastmock 准备好接口假数据,并在api 目录中请求数据,组件中不做数据请求:数据

  • iconfont 选择需要的icon 相似即可,解压放assets 目录下

2. 移动端适配

  • 移动端页面开发当然少不了适配

    init();

    window.addEventListener('resize', init);

      - 在src 下创建目录modules 创建rem.js如下:
      ```js
      document.documentElement.style.fontSize = 
          document.documentElement.clientWidth / 3.75 + 'px';
      // 横竖屏切换
      window.onresize = function() {
          document.documentElement.style.fontSize = 
              document.documentElement.clientWidth / 3.75 + 'px';
      }

    • 在public 目录下创建js 文件adapter.js 内容如下:

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

  • index.html中引用adapter.js ,main.jsx 中引用rem.js

    3. 实现父组件 SelectChannel

  • 除了子组件独有的部分,数据状态改变和函数都在父组件里进行,传给子组件,完整文件如下:

    export default function SelectChannel() {
    
     const [list, setList] = useState([
         {
             id: 7,
             title: '大别野',
             img: 'https://bbs.mihoyo.com/_nuxt/img/game-dby.7b16fa8.jpg',
             checked: true,
         },
     ]);
     const [loading,setLoading] = useState(false)
     const [change,setChange] = useState(false)
    
     // 筛选出已选择和未选择项
     const TrueCheck = list.filter(item => item.checked == true);
     const FalseCheck = list.filter(item => item.checked == false);
    
     // 提示模态框
     const modal=()=>{
         return(
             loading && 
             <Modal>
                 <span>至少选择一个游戏哦~</span>
             </Modal>
         )
         }
     // 定时让模态框消失
     const setState = () =>{
         setTimeout(()=>{
             setLoading(false)
         },2000)
     }
    
     // 选择
     const choose = item => {
         // console.log('--------');
         let idx = list.findIndex(data => item.id === data.id);
         // console.log(idx);
         list[idx].checked = !list[idx].checked;
         setList([...list]);
         setChange(true)
     };
    
     // 删除已选择项
     const deleteList = item => {
         let idx = list.findIndex(data => item.id === data.id);
         // 判断已选择项是否小于或等于两个,若是,那么不可删除,弹出提示模态框,若大于两个则执行删除
         if(TrueCheck.length <= 2){
             setLoading(true);
             setState();
         }else{
             list[idx].checked = !list[idx].checked;
             setList([...list]);
             setChange(true)
         }
     };
    
     // 拿取数据
     useEffect(() => {
         (async () => {
             let { data } = await select();
             // console.log(data);
             setList([...list, ...data]);
         })();
     }, []);
    
     // 拖拽后排序
     const handleDragEnd = ({active, over}) => {
         if(active.id !== over.id){
             setList((items) => {
             const oldIndex = items.findIndex(item => item.id === active.id)
             const newIndex = items.findIndex(item => item.id === over.id)
             return arrayMove(items, oldIndex, newIndex)
         })
         }
         setChange(true)
     }
    
     return (
         <>
             {modal()}
             <Header change={change} />
             <Content data={list} 
                 deleteList={deleteList} 
                 handleDragEnd={handleDragEnd} 
                 />
             <Footer data={list} 
                 choose={choose} 
                 FalseCheck={FalseCheck} 
                 />
         </>
     );

    3.1 小模态框

  • 给小模态框组件一个状态loading 默认为false 当触发删除函数时判断我的频道中数组数据长度,改变loading 状态

      const [loading,setLoading] = useState(false)
      const deleteList = item => {
          let idx = list.findIndex(data => item.id === data.id);
          // 判断已选择项是否小于或等于两个,若是,那么不可删除,弹出提示模态框,若大于两个则执行删除
          if(TrueCheck.length <= 2){
              setLoading(true);
              setState();
          }else{
              list[idx].checked = !list[idx].checked;
              setList([...list]);
              setChange(true)
          }
      };

  • 我的频道中数组数据长度只剩两个时再点击删除会弹出提示,由原页面可知整个页面就这一个提示数据,所以写死就可

      const [loading,setLoading] = useState(false)
      // 提示模态框
      const modal=()=>{
          return(
              loading && 
              <Modal>
              // 没有其他弹出项,弹出数据写死
                  <span>至少选择一个游戏哦~</span>
              </Modal>
          )
          }
      // 定时让模态框消失
      const setState = () =>{
          setTimeout(()=>{
              setLoading(false)
          },2000)
      }

    3.2 删除和添加函数

  • 逻辑一样,findIndex 找出list 中的数据,将其和子组件触发事件传过来的 item 的id 进行对比,改变找出数据的checked ,setList 即可实现两个组件显示列表数据的改变

      // 选择
      const choose = item => {
          // console.log('--------');
          let idx = list.findIndex(data => item.id === data.id);
          // console.log(idx);
          list[idx].checked = !list[idx].checked;
          setList([...list]);
          setChange(true)
      };
    
      // 删除已选择项
      const deleteList = item => {
          let idx = list.findIndex(data => item.id === data.id);
          // 判断已选择项是否小于或等于两个,若是,那么不可删除,弹出提示模态框,若大于两个则执行删除
          if(TrueCheck.length <= 2){
              setLoading(true);
              setState();
          }else{
              list[idx].checked = !list[idx].checked;
              setList([...list]);
              setChange(true)
          }
      };

    3.3 拖拽后排序

  • 逻辑和删除添加大致相同,调用了 dnd-kit 中的arrayMove 函数,对交换后的数据进行处理

      // 拖拽后排序
      const handleDragEnd = ({active, over}) => {
          if(active.id !== over.id){
              setList((items) => {
              const oldIndex = items.findIndex(item => item.id === active.id)
              const newIndex = items.findIndex(item => item.id === over.id)
              return arrayMove(items, oldIndex, newIndex)
          })
          }
          setChange(true)
      }

    4. 页面头部tab

  • 布局常见的三列式布局,左右两个地方可点击跳转首页,这里可以设置路由,使用Link 但这里就展示独立的一个页面组件开发,先用a 标签代替,后续若需要可替换

  • 使用classnames 可以十分简单的设置动态类名,利用父组件中传过来的 chang 值对“确认”按钮是否高亮做出改变 代码如下:

    export default function Header({change}) {
      return (
          <Tab>
              <div className="left">
                  <a href="#">
                      <i className="iconfont icon-fanhui"></i>
                  </a>
              </div>
              <div className="content">首页频道选择</div>
              <div className="right">
                  <a href="#" className={classnames("noChange",{changeItem: change})}>
                      确定
                  </a>
              </div>
          </Tab>
      );
    }

    5. 我的频道和推荐频道组件实现

    5.1 组件分析

    我的频道和推荐频道都有两个部分,一个固定的头,显示我的频道和推荐频道标题,标题下方是map 动态生成的列表组件,我的频道还需要拖拽排序,遂这里都相应再增加了个子组件 ContentList

    5.2 拖拽排序组件库选择

  • 这个组件是整个组件实现的难点,拖拽排序自己实现很难,我尝试自己用原生react 实现了下,效果不尽人意,最终决定用现成的方案,常见的拖拽库选择有下:

    • react-dnd guthub 中十分受欢迎的一个拖拽库,功能十分完备,但是用于本页面貌似有点太“重”了,遂放弃

    • react-beautiful-dnd 和react-dnd 类似,但是我下载包貌似不支持react18,install 不下来,遂寄

    • dnd-kit 芜湖,看了下官方官方文档使用十分简单,只需要用DndContext、 SortableContext 包装拖拽根组件,Sensors 监听不同的拖动设备,再加上组件库现成的碰撞算法即可,十分滴简单

      5.3 我的频道组件实现

      5.3.1 父组件实现

  • 使用@dnd-kit/core 中的hook useSensor捕获传感器

  • 使用@dnd-kit/core 中的 DndContext SortableContext 组件包装拖拽根组件

  • 使用@dnd-kit/modifiers 中的 verticalListSortingStrategy 动态修改传感器检测到的运动坐标,限制拖拽方向为纵向 父组件代码如下:

    export default function Content(props) {
    
      const { data, deleteList, handleDragEnd } = props
    
      // 捕获触摸传感器
      const touchSensor = useSensor(TouchSensor,{
          activationConstraint:{
              delay: 300,
              tolerance: 10,
          }
      })
      // 捕获鼠标
      const mouseSensor = useSensor(MouseSensor,{
          activationConstraint:{
              delay: 300,
              tolerance: 0,
          }
      })
    
      const sensors = useSensors(
          touchSensor,
          mouseSensor
      )
    
      return (
          <BodyWrapper>
          <TabWrapper>
              <header>
                  <div className='left'>
                      <p>我的频道</p>
                  </div>
                  <div className='right'>
                      <p>长按拖动排序</p>
                  </div>
              </header>
          </TabWrapper>
          // DndContext SortableContext 包装拖拽根组件
          <DndContext
              sensors={sensors}
              collisionDetection={closestCenter}
              onDragEnd={handleDragEnd}
              modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
          >
          <SortableContext
              items={data.map(item => item.id)}
              strategy={verticalListSortingStrategy}
          >
              {
                  data.map((item) =>
                  <ContentList key={item.id} 
                      deleteList={deleteList} 
                      item={item}
                      {...item}
                  />
                  )
              }
          </SortableContext>
          </DndContext>
          </BodyWrapper>
      );

    5.3.2 子组件实现

  • 使用@dnd-kit/sortable 中的hook useSortable 匹配父元素id 参数

  • 使用@dnd-kit/utilities 中的CSS 搭配一些css 属性实现选中拖拽时的样式 代码如下:

      - 在src 下创建目录modules 创建rem.js如下:
      ```js
      document.documentElement.style.fontSize = 
          document.documentElement.clientWidth / 3.75 + 'px';
      // 横竖屏切换
      window.onresize = function() {
          document.documentElement.style.fontSize = 
              document.documentElement.clientWidth / 3.75 + 'px';
      }0

    官方拖拽时没有样式改变我这给了个0.6的透明

    5.4 推荐频道组件实现

  • 除了没有拖拽排序外几乎和我的频道一样

  • 判断FalseCheck 数组长度以控制组件是否显示,若组件列表中没有数据了,不显示组件 代码如下:

    5.4.1 父组件

      - 在src 下创建目录modules 创建rem.js如下:
      ```js
      document.documentElement.style.fontSize = 
          document.documentElement.clientWidth / 3.75 + 'px';
      // 横竖屏切换
      window.onresize = function() {
          document.documentElement.style.fontSize = 
              document.documentElement.clientWidth / 3.75 + 'px';
      }1

    5.4.2 子组件

      - 在src 下创建目录modules 创建rem.js如下:
      ```js
      document.documentElement.style.fontSize = 
          document.documentElement.clientWidth / 3.75 + 'px';
      // 横竖屏切换
      window.onresize = function() {
          document.documentElement.style.fontSize = 
              document.documentElement.clientWidth / 3.75 + 'px';
      }2

    最终效果:

React组件设计,仿米游社首页频道设置页面  第2张

React组件设计,仿米游社首页频道设置页面  第3张

最终目录结构:

  - 在src 下创建目录modules 创建rem.js如下:
  ```js
  document.documentElement.style.fontSize = 
      document.documentElement.clientWidth / 3.75 + 'px';
  // 横竖屏切换
  window.onresize = function() {
      document.documentElement.style.fontSize = 
          document.documentElement.clientWidth / 3.75 + 'px';
  }3

最后

这就是这次组件实现的全过程,后续会继续完善,代码在仿米游社首页频道设置页面
github page 直接查看效果:实时演示

原文:https://juejin.cn/post/7113029634295513101
打赏
海报

本文转载自互联网,旨在分享有价值的内容,文章如有侵权请联系删除,部分文章如未署名作者来源请联系我们及时备注,感谢您的支持。

转载请注明本文地址:https://www.shouxicto.com/article/6174.html

相关推荐

ffplay视频播放原理分析

ffplay视频播放原理分析

   前言    作为一个刚接触react 组件设计不久的新人,独立完成一个组件的设计开发其中过程是十分卡手的,本篇详尽的描...

前端 2022.08.07 0 59

发布评论

ainiaobaibaibaibaobaobeishangbishibizuichiguachijingchongjingdahaqiandaliandangaodw_dogedw_erhadw_miaodw_tuzidw_xiongmaodw_zhutouganbeigeiliguiguolaiguzhanghahahahashoushihaixiuhanheixianhenghorse2huaixiaohuatonghuaxinhufenjiayoujiyankeaikeliankouzhaokukuloukunkuxiaolandelinileimuliwulxhainiolxhlikelxhqiuguanzhulxhtouxiaolxhwahahalxhzanningwennonuokpinganqianqiaoqinqinquantouruoshayanshengbingshiwangshuaishuijiaosikaostar0star2star3taikaixintanshoutianpingtouxiaotuwabiweifengweiquweiwuweixiaowenhaowoshouwuxiangjixianhuaxiaoerbuyuxiaokuxiaoxinxinxinxinsuixixixuyeyinxianyinyueyouhenghengyuebingyueliangyunzanzhajizhongguozanzhoumazhuakuangzuohenghengzuoyi
支付宝
微信
赞助本站