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


    前言

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

    准备阶段

    页面分析

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

    • 监听列表数据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 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用工具

    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. 移动端适配

    • 移动端页面开发当然少不了适配* 在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';};init();window.addEventListener('resize', init); * 在src 下创建目录modules 创建rem.js如下:document.documentElement.style.fontSize = document.documentElement.clientWidth / 3.75 + 'px';// 横竖屏切换window.onresize = function() {document.documentElement.style.fontSize = document.documentElement.clientWidth / 3.75 + '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 &&   至少选择一个游戏哦~  ) } // 定时让模态框消失 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()} 
    );
    • 1

    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)}}; 
    
    • 1
    • 我的频道中数组数据长度只剩两个时再点击删除会弹出提示,由原页面可知整个页面就这一个提示数据,所以写死就可
     const [loading,setLoading] = useState(false)// 提示模态框const modal=()=>{return(loading && // 没有其他弹出项,弹出数据写死至少选择一个游戏哦~)}// 定时让模态框消失const setState = () =>{setTimeout(()=>{setLoading(false)},2000)} 
    
    • 1

    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)}}; 
    
    • 1

    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)} 
    
    • 1

    4. 页面头部tab

    • 布局常见的三列式布局,左右两个地方可点击跳转首页,这里可以设置路由,使用Link 但这里就展示独立的一个页面组件开发,先用a 标签代替,后续若需要可替换
    • 使用classnames 可以十分简单的设置动态类名,利用父组件中传过来的 chang 值对“确认”按钮是否高亮做出改变

    代码如下:

    export default function Header({change}) {return (
    首页频道选择
    ); }
    • 1
    • 2

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

    5.1 组件分析

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

    5.2 拖拽排序组件库选择

    • 这个组件是整个组件实现的难点,拖拽排序自己实现很难,我尝试自己用原生react 实现了下,效果不尽人意,最终决定用现成的方案,常见的拖拽库选择有下:* react-dnd github 中十分受欢迎的一个拖拽库,功能十分完备,但是用于本页面貌似有点太“重”了,遂放弃* 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 (

    我的频道

    长按拖动排序

    // DndContext SortableContext 包装拖拽根组件 item.id)}strategy={verticalListSortingStrategy}>{data.map((item) =>)}
    );
    • 1

    5.3.2 子组件实现

    • 使用@dnd-kit/sortable 中的hook useSortable 匹配父元素id 参数
    • 使用@dnd-kit/utilities 中的CSS 搭配一些css 属性实现选中拖拽时的样式

    代码如下:

    export default function ContentList(props) {const { checked, id, title, img, deleteList, item } = props;const {setNodeRef,attributes,listeners,transition,transform,isDragging} = useSortable({id: id})// 长按选中元素拖动时样式const style = {transition,transform: CSS.Transform.toString(transform),// 拖拽时透明度,原版为1opacity: isDragging ? 0.6 : 1,dragSelectorExclude: "i"}return (<>{checked == true &&{title}{title !== '大别野' &&  deleteList(item)} >}}) 
    
    • 1

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

    5.4 推荐频道组件实现

    • 除了没有拖拽排序外几乎和我的频道一样
    • 判断FalseCheck 数组长度以控制组件是否显示,若组件列表中没有数据了,不显示组件

    代码如下:

    5.4.1 父组件

    export default function Footer(props) {const { data, choose, FalseCheck } = propsreturn ({FalseCheck.length > 0 &&

    推荐频道

    }
    ); }
    • 1
    • 2

    5.4.2 子组件

    export default function ContentList(props) {const { data , choose } = propsreturn ({data.map((item) => item.checked == false &&{item.title} choose(item)}>)})
    } 
    
    • 1
    • 2

    最终效果:

    最终目录结构:

    select-channel
    ├─ index.html
    ├─ package-lock.json
    ├─ package.json
    ├─ public
    │└─ js
    │ └─ adapter.js
    ├─ src
    │├─ api
    ││└─ request.js
    │├─ App.css
    │├─ App.jsx
    │├─ assets
    ││├─ font
    ││└─ styles
    ││ └─ reset.css
    │├─ components
    ││└─ 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
    │├─ index.css
    │├─ main.jsx
    │└─ modules
    │ └─ rem.js
    └─ vite.config.js 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    最后

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

  • 相关阅读:
    360 评估反馈问题的示范案例
    Leetcode 86. Partition List (链表好题)
    OSPF协议
    网上商城项目(加入购物车)
    CQDs碳量子点,溶于甲苯碳量子点,发射波长630nm
    Spring监听器
    [HNOI2010]弹飞绵羊【LCT】
    LAMP架构-nginx并发优化
    【【萌新的SOC学习之AXI接口简介】】
    基于虚拟化技术的5G核心网资源配置算法
  • 原文地址:https://blog.csdn.net/web2022050901/article/details/126373837