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
posted @ 2022-03-10 00:18  沧浪浊兮  阅读(2070)  评论(3编辑  收藏  举报