React元件設計,仿米遊社首頁頻道設定頁面

語言: CN / TW / HK

theme: channing-cyan

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第1天,點選檢視活動詳情

前言

作為一個剛接觸react 元件設計不久的新人,獨立完成一個元件的設計開發其中過程是十分卡手的,本篇詳盡的描述了米遊社首頁頻道選擇頁面元件開發的全過程,希望這個這個簡單元件的設計開發能對和我一樣接觸react元件開發不久的人有點幫助

準備階段

頁面分析

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

演示.gif 佈局十分常見,頭、身、尾,三部分,對應三個元件,點選推薦頻道元件中的新增符號,可以新增到我的頻道元件中,我的頻道中的列表資料可以長按進行拖拽排序,原頁面那個控制代碼符號好像就是提示用,沒有實際功能作用,整個列表長按都可以拖拽,當刪除到最後一個遊戲時會有一個小的模態框提示,原頁面資料發生改變右上角確定高亮,綜上我們需要完成:
- 監聽列表資料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. 移動端適配

  • 移動端頁面開發當然少不了適配

    • 在public 目錄下建立js 檔案adapter.js 內容如下: ```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如下: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 - 除了子元件獨有的部分,資料狀態改變和函式都在父元件裡進行,傳給子元件,完整檔案如下:js 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()}


    ); ```

    3.1 小模態框

    • 給小模態框元件一個狀態loading 預設為false 當觸發刪除函式時判斷我的頻道中陣列資料長度,改變loading 狀態 js 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) } };
    • 我的頻道中陣列資料長度只剩兩個時再點選刪除會彈出提示,由原頁面可知整個頁面就這一個提示資料,所以寫死就可 js 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 即可實現兩個元件顯示列表資料的改變 ```js // 選擇 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 函式,對交換後的資料進行處理 js // 拖拽後排序 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 值對“確認”按鈕是否高亮做出改變 程式碼如下: js 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 動態修改感測器檢測到的運動座標,限制拖拽方向為縱向 父元件程式碼如下: ```js 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) => ) } ); ```

    5.3.2 子元件實現

    • 使用@dnd-kit/sortable 中的hook useSortable 匹配父元素id 引數
    • 使用@dnd-kit/utilities 中的CSS 搭配一些css 屬性實現選中拖拽時的樣式 程式碼如下: ```js 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), // 拖拽時透明度,原版為1 opacity: isDragging ? 0.6 : 1, dragSelectorExclude: "i" }

    return ( <> { checked == true && {title} { title !== '大別野' && deleteList(item)} > } }
    ) ``` ~~官方拖拽時沒有樣式改變我這給了個0.6的透明~~

    5.4 推薦頻道元件實現

    • 除了沒有拖拽排序外幾乎和我的頻道一樣
    • 判斷FalseCheck 陣列長度以控制組件是否顯示,若元件列表中沒有資料了,不顯示元件 程式碼如下:

    5.4.1 父元件

    ```js export default function Footer(props) {

    const { data, choose, FalseCheck } = props

    return ( { FalseCheck.length > 0 &&

    推薦頻道

    } ); } ```

    5.4.2 子元件

    ```js export default function ContentList(props) {

    const { data , choose } = props

    return ( { data.map((item) => item.checked == false && {item.title} choose(item)}> ) } ) } ```

    最終效果:

實現.gif

演示2.gif

最終目錄結構:

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

最後

這就是這次元件實現的全過程,後續會繼續完善,程式碼在仿米遊社首頁頻道設定頁面
github page 直接檢視效果:實時演示