React全家桶開發「新聞后台管理項目」實戰(前端項目+源碼)


本項目1.0完成於2022年3月8日,請注意時效~
// 暫不可用!項目部署預覽地址:點擊查看
// 暫不可用!項目Github地址(包含所有源碼、數據):點擊查看如有疑問請私信博客,期待你的star!
項目Gitee地址(包含所有源碼、數據):點擊查看如有疑問請私信博客,期待你的star!
React教程參考:千鋒2022版React全家桶教程

一、項目目標

1.1 個人期望

經過了下列文檔&刷題,期望實戰中提高。

技術目標:使用React全家桶

  • React組件開發
  • React Hooks
  • React Router
  • Recat Redux
  • Antd組件庫

1.2 產品選擇

可借鑒的有:網易雲音樂PC項目、后台管理系統項目。結合使用帆軟數據可視化產品的經驗,選擇做后台管理項目。業務交互選擇新聞后台管理。

1.3 項目描述

實現一個新聞發布管理平台,業務目標:

  • 用戶登錄
  • 游客訪問:瀏覽新聞
  • 用戶管理:新增用戶、修改用戶、刪除用戶、禁用用戶
  • 權限管理:角色管理、頁面訪問權限控制、側邊欄權限控制
  • 新聞業務:撰寫新聞、草稿箱、新聞審核、新聞發布及下線等

1.4 適合人群

  • 對前端有興趣
  • 對HTML/CSS/JS/REACT有一定了解
  • 希望有一定的的那個項目經驗

1.5 推薦用時

60h~100h

二、技術選型

三、項目模塊文檔

3.1 登錄

  實現用戶登錄功能:用戶進入登陸頁面,輸入必填項賬號及密碼,點擊登錄校驗賬號密碼,登錄成功后保存狀態,跳轉至home頁面;若登陸失敗彈出“用戶名或密碼不匹配”。
  實現效果:
(首頁使用了粒子效果太大了,展示不出來)

3.2 首頁

  首頁展示四個模塊:用戶最常瀏覽、用戶點贊最多、用戶信息、新聞分類。用戶最常瀏覽模塊展示瀏覽量最多的6個新聞標題;用戶點贊最多模塊展示點贊量最多的6個新聞標題;用戶信息展示用戶頭像&名稱&角色&地區,並設有按鈕彈出展示該用戶已發布新聞分類的餅圖;新聞分類使用柱狀圖展示所有用戶的新聞分類數量。其中,新聞標題可點擊預覽新聞內容。
  實現效果:
image

3.3 用戶管理

  用戶管理頁面展示用戶信息列表及用戶操作:包括新增用戶、區域篩選、用戶狀態開關、刪除用戶、編輯用戶等。用戶信息列表展示區域、角色、名稱、狀態、操作(刪除、編輯)。超級管理員可以添加、刪除、編輯所有用戶;區域管理員盡可以新增、刪除、編輯本用戶及本區域下的區域編輯用戶;區域編輯沒有本頁面權限。
image

3.4 權限管理

  權限管理包含兩個頁面:角色列表;權限列表。
  角色列表展示角色ID、角色名稱、角色操作(刪除角色、編輯角色權限)。
  權限列表展示頁面ID、權限名稱、權限路徑、操作(刪除路徑、路徑配置狀態)。
  實現效果:
image

3.5 新聞管理

  新聞管理包含三個頁面:撰寫新聞、草稿箱、新聞分類。
  撰寫新聞包括:新聞標題、新聞分類(下拉選擇)、新聞內容、新聞提交。其中新聞提交要包括保存草稿箱及提交審核兩個操作。
  點擊保存草稿箱,跳轉至草稿箱頁面,並在右下側通知用戶相關消息,草稿箱頁面顯示新聞ID、新聞標題(新聞標題可點擊預覽新聞內容)、作者、分類、操作(刪除、修改、提交審核)。
  點擊提交審核,將跳轉至審核管理-審核列表,並在右下側通知用戶相關消息。
  新聞分類頁面展示分類ID、分類名稱(可修改)、操作(刪除)。
  實現效果:
image

3.6 審核管理

  審核管理包括兩個頁面:審核新聞、審核列表。
  審核新聞頁面展示待審核的新聞項,內容有:新聞標題、作者、分類、操作(通過、駁回)。點擊通過或駁回在右下側通知用戶相關消息。
  審核列表展示本用戶在審核階段的新聞,內容有:新聞標題、作者、分類、審核狀態、操作。若審核狀態為未通過、操作為更新;若審核狀態為已通過、操作為發布。點擊更新可編輯新聞內容(類似撰寫新聞的頁面);點擊發布則跳轉至已發布頁面,並在右下側通知用戶相關消息。
  實現效果:
image

3.7 發布管理

  發布管理包括三個頁面:待發布、已發布、已下線。
  待發布頁面展示本用戶審核通過仍未發布的新聞,內容有新聞標題、作者、分類、操作(發布)。
  已發布頁面展示本用戶已發布的新聞,內容有新聞標題、作者、分類、操作(下線)。
  已下線頁面展示本用戶已下線的新聞,內容有新聞標題、作者、分類、操作(刪除)。
  實現效果:
image

四、項目規范

  1. 文件夾、文件名稱統一小寫
  2. JS組件采用大駝峰命名,比那輛采用小駝峰命名
  3. 使用hooks編寫
  4. 豐富注釋
  5. rudux:每個模塊有自己獨立的reducer,通過combineReducer進行合並

五、技術文檔

功能 接口地址 調用案例
用戶 /users 獲取用戶及其角色權限/users?_expand=role
角色權限 /roles
子菜單 /children
父菜單 /rights 取父子菜單/rights?_embed=children
新聞分類 /categories
區域 /regions
新聞 /news 獲取對應新聞內容、分類及作者權限/news/${id}?_expand=category&_expand=role
// 審核狀態、發布狀態映射(數組id即為狀態碼)
const auditList = ["未審核", '審核中', '已通過', '未通過']
const publishList = ["未發布", '待發布', '已上線', '已下線']
  • 路由架構
    image
// V6實例
import React from 'react'
import {
    HashRouter as Router,
    Routes,
    Route,
    Navigate
} from "react-router-dom"
import Login from '../views/login/Login'
import NewsSandBox from '../views/sandbox/NewsSandBox'
import News from '../views/news/News'
import Detail from '../views/news/Detail'

export default function IndexRouter() {

    return (
        <Router>
            <Routes>
                <Route path="/login" element={<Login />} />
                <Route path="/news" element={<News />}/>
                <Route path="/detail/:id" element={<Detail />}/>
                <Route path="/*" element={localStorage.getItem("token")?<NewsSandBox />:<Navigate to="/login" />} />
             {/*
                {localStorage.getItem("token")?<Route path="/" element={<NewSandBox />} />:<Route path="*" element={<Navigate to="/login" />} />}
            */}
            </Routes>
        </Router>
    )
}
  • 簡單數據處理:使用lodash進行簡單數據處理
renderBarView(_.groupBy(res.data, item => item.category.title))
  • 頂欄控制側邊欄伸縮
// 頂欄組件
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>{
   return {
    isCollapsed
   }
}
const mapDispatchToProps = {
  changeCollapsed(){
    return {
      type:"change_collapsed"
    }
  }
}

export default connect(mapStateToProps,mapDispatchToProps)(TopHeader)
// 側邊欄組件
// 側邊欄伸縮,使用connect
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>({isCollapsed})
export default connect(mapStateToProps)(SideMenu)

實現效果:
image

  • 數據加載Loading,狀態持久化
// Redux store設置,使用黑名單避免&實現持久化
import {createStore,combineReducers} from 'redux'
import {CollapsedReducer} from './reducers/CollapsedReducer'
import {LoadingReducer} from './reducers/LoadingReducer'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web

const persistConfig = {
    key: 'hangyi',
    storage,
    blacklist: ['LoadingReducer']
}
const reducer = combineReducers({
    CollapsedReducer,
    LoadingReducer
})
const persistedReducer = persistReducer(persistConfig, reducer)

const store = createStore(persistedReducer);
const persistor = persistStore(store)
export {
    store,
    persistor
}
// Reducer設置
export const CollapsedReducer = (prevState={
    isCollapsed:false
},action)=>{
    let {type} =action

    switch(type){
        case "change_collapsed":
            let newstate = {...prevState}
            newstate.isCollapsed = !newstate.isCollapsed
            return newstate
        default:
            return prevState
    }
}
  • JSON Server方法
//取數據  get
        // axios.get("http://localhost:8000/posts/2").then(res=>{
        //     console.log(res.data)
        // })

        // 增  post
        // axios.post("http://localhost:8000/posts",{
        //     title:"33333",
        //     author:"xiaoming"
        // })

        // 更新 put

        // axios.put("http://localhost:8000/posts/1",{
        //     title:"1111-修改"
        // })

        // 更新 patch
        // axios.patch("http://localhost:8000/posts/1",{
        //     title:"1111-修改-11111"
        // }) 

        // 刪除  delete
        // axios.delete("http://localhost:8000/posts/1")

        // _embed
        // axios.get("http://localhost:8000/posts?_embed=comments").then(res=>{
        //     console.log(res.data)
        // })

        // _expand
        // axios.get("http://localhost:8000/comments?_expand=post").then(res=>{
        //     console.log(res.data)
        // })
  • 權限:頁面本身權限配置+用戶角色權限配置
    image
// 解構當前用戶的頁面權限
  const {role: {rights}} = JSON.parse(localStorage.getItem("token"));
  // 檢查登錄用戶頁面權限方法
  const checkPagePermission = (item) => {
    return item.pagepermisson && rights.includes(item.key)
  };
   // 導航方法
  const navigate = useNavigate();
  // 截取當前URL路徑
  const location = useLocation();
  const selectedkeys = location.pathname;
  const openkeys = ["/" + location.pathname.split("/")[1]];
  // 側邊欄內容列表
  const renderMenu = (menuList) => {
    return menuList.map(item => {
      // 檢查每一項是否有下級列表(使用可選鏈語法)&& 頁面權限
      if (item.children?.length > 0 && checkPagePermission(item)) {
        return <SubMenu key={item.key} icon={iconList[item.key]} title={item.title}>
          {renderMenu(item.children)}
        </SubMenu>
      }
      return checkPagePermission(item) && <Menu.Item key={item.key} icon={iconList[item.key]} onClick={() =>
        navigate(item.key)
      }>{item.title}</Menu.Item>
    })
  }
  • 多級用戶管理:在添加用戶、編輯用戶時,超級管理員可以隨意添加、區域管理員可以添加編輯本人及本區域下的區域編輯。
// 代碼節選
const {roleId,region,username} = JSON.parse(localStorage.getItem("token"));
  // 初始化用戶權限列表
  useEffect(() => {
    const roleObj = {
      "1":"superadmin",
      "2":"admin",
      "3":"editor"
  }
    axios.get("/users?_expand=role").then(res => {
      const list = res.data;
      setdataSource(roleObj[roleId]==="superadmin"?list:[
        // 超級管理員不限制,區域管理員:自己+自己區域編輯,區域編輯:看不到用戶列表
        ...list.filter(item=>item.username===username),
        ...list.filter(item=>item.region===region&& roleObj[item.roleId]==="editor")
    ])
    })
  }, [roleId,region,username]);
  
// 控制區域、角色的新增&編輯權限
// 父組件傳遞 regionList={regionList} roleList={roleList}等參數
<Modal title="添加用戶" okText="確定" cancelText="取消" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
        <UserForm ref={addForm} regionList={regionList} roleList={roleList} />
      </Modal>

      <Modal title="更新用戶" okText="更新" cancelText="取消" visible={isUpdateVisible} onOk={updateOk} onCancel={updateCancel}>
      <UserForm ref={updateForm} regionList={regionList} roleList={roleList} isUpdateDisabled={isUpdateDisabled} />
    </Modal>
	
// 子組件props.regionList
 <Select disabled={isDisabled} >
                    {props.regionList.map(item => {
                        return <Option value={item.value} key={item.id} disabled={checkRegionDisabled(item)}>{item.title}</Option>
                    })}
                </Select>
  • 可編輯單元格
// 參考https://ant.design/components/table-cn/#components-table-demo-edit-cell
    // 使用Context來實現跨層級的組件數據傳遞
    const EditableContext = React.createContext(null);
    const EditableRow = ({ index, ...props }) => {
        const [form] = Form.useForm();
        return (
            <Form form={form} component={false}>
                <EditableContext.Provider value={form}>
                    <tr {...props} />
                </EditableContext.Provider>
            </Form>
        );
    };
    const EditableCell = ({
        title,
        editable,
        children,
        dataIndex,
        record,
        handleSave,
        ...restProps
    }) => {
        const [editing, setEditing] = useState(false);
        const inputRef = useRef(null);
        const form = useContext(EditableContext);
        useEffect(() => {
            if (editing) {
                inputRef.current.focus();
            }
        }, [editing]);

        const toggleEdit = () => {
            setEditing(!editing);
            form.setFieldsValue({
                [dataIndex]: record[dataIndex],
            });
        };

        const save = async () => {
            try {
                const values = await form.validateFields();
                toggleEdit();
                handleSave({ ...record, ...values });
            } catch (errInfo) {
                console.log('Save failed:', errInfo);
            }
        };

        let childNode = children;

        if (editable) {
            childNode = editing ? (
                <Form.Item
                    style={{
                        margin: 0,
                    }}
                    name={dataIndex}
                    rules={[
                        {
                            required: true,
                            message: `${title} is required.`,
                        },
                    ]}
                >
                    <Input ref={inputRef} onPressEnter={save} onBlur={save} />
                </Form.Item>
            ) : (
                    <div
                        className="editable-cell-value-wrap"
                        style={{
                            paddingRight: 24,
                        }}
                        onClick={toggleEdit}
                    >
                        {children}
                    </div>
                );
        }

        return <td {...restProps}>{childNode}</td>;
    };
···
<Table dataSource={dataSource} columns={columns}
                pagination={{
                    pageSize: 5
                }}
                rowKey={item => item.id}

                 components={{
                     body: {
                         row: EditableRow,
                         cell: EditableCell,
                     }
                 }}

            />

六、疑難巧點

6.1 react routerV6新版本

  • Routes代替Switch
  • element代替component:
// V6
element={<Login />}
// history
component={Login}
  • Navigate干掉了Redirect:
// V6
<Route path="/*" element={localStorage.getItem("token")?<NewsSandBox />:<Navigate to="/login" />} />
// history
<Route path="/" render={()=>localStorage.getItem("token")?<NewsSandBox ></NewsSandBox>:<Redirect to="/login"/>}/>
  • useNavigate, useLocation等代替withRouter,props.history.push等的方法
// V6
const location = useLocation();
const selectedkeys = location.pathname;
const openkeys = ["/" + location.pathname.split("/")[1]];
// history
const selectKeys = [props.location.pathname]
const openKeys = ["/"+props.location.pathname.split("/")[1]]

// 導航方法
// V6
const navigate = useNavigate();
navigate(item.key)
// history
props.history.push(item.key)

6.2 axios攔截

可以實現:

  • 簡化每次axios請求的代碼量
  • 實現加載數據時有提示
import axios from 'axios'
import {store} from '../redux/store'
axios.defaults.baseURL="http://localhost:5000"

// axios.defaults.headers

// axios.interceptors.request.use
// axios.interceptors.response.use
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    // 顯示loading
    store.dispatch({
        type:"change_loading",
        payload:true
    })
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    store.dispatch({
        type:"change_loading",
        payload:false
    })
    //隱藏loading
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    store.dispatch({
        type:"change_loading",
        payload:false
    })
     //隱藏loading
    return Promise.reject(error);
  });

七、待補充

  1. 性能優化:useMemo、useCallback和memo等
  2. redux hooks

八、附錄

8.1 命令表

// 創建react-app
npx create-react-app my-app
// 進入該目錄
cd my-app
// 啟動工程
npm start
// npm安裝相關依賴(例如antd)
npm i --save antd
// JSON Server啟動(使用db.json文件,本地5000端口)
json-server --watch db.json --port 5000


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM