基于Ant design pro react 实现的动态路由跳转三级级联写作工作台源码

这是最近在做的一个项目的写作工作台模块,风格类似于简书,但略有不同,测试版略显粗糙,不过已经可以使用了。本模块采用Ant design pro react v4脚手架开发完成,富文本编辑插件采用时下火热并且可商用的tinymce,采用本地化部署tinymce插件。与SPA不同的是,针对路由参数做了优化,在级联切换时URL会跟随变化,变化的是级联节点的ID,这样就满足了基本的seo需求。
另外,针对SPA客户端渲染问题做了seo优化,加入了title、keywords、description(TDK)标签,并没有采用SSR(服务端渲染),因为那违背了前后端分离的初衷,搞得前端比后端还重。Antd做了较重的封装,为了快速完成项目也只能将就了,项目上线后应该会考虑构建自己的研发平台,而且不会采用过度封装,毕竟任何规则终将成为自己的藩篱。自由、简捷和可持续生长永远是框架不老的传说,也是追求卓越的毕生追求,这世界一套系统就够了。
PC端效果如下:
年谱工作台

移动端效果如下:

全屏后的编辑器:

废话表过,上代码:

/********************主组件book.jsx:*******************************/

import React, {Component} from 'react';
import {connect} from 'umi';
import {Button} from 'antd';
import {PlusOutlined} from '@ant-design/icons';
import styles from './style.less';
import HomeOutlined from "@ant-design/icons/HomeOutlined";
import Chapter from "@/pages/book/components/chapter";
import BookList from "@/pages/book/components/booklist";
import BookView from "@/pages/book/components/base";

class Book extends Component {
    main = undefined;

    constructor(props) {
        super(props);
        this.state = {
            mode: 'inline',
        };
    }

    componentDidMount() {
        this.query(this.props.dispatch, this.props.match);
        window.addEventListener('resize', this.resize);
        this.resize();
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.resize);
    }

    query = (dispatch, match) => {
        const {path, params} = match;
        if (path === '/space/book') {
            this.queryBooks(dispatch, params, (res) => {
                if (res.status === 'ok') {
                    if (res.data && res.data.length > 0) {
                        this.queryCurrentBook(dispatch, {
                                bookId: res.data[0].bookId
                            },
                            (resp) => this.callback4Chapter(dispatch, resp),
                        )
                    }
                }
            });
        } else if (path === '/space/book/:bookId/chapter/:chapterId') {
            if (this.props.bookSpace.book.length === 0) {
                this.queryBooks(dispatch, params, null);
            }
            if (this.props.bookSpace.currentBook &&
                this.props.bookSpace.currentBook.bookId === undefined) {
                this.queryCurrentBook(dispatch, params, null);
            }
            console.log('queryCurrentChapter>>>', match, params);
            this.queryCurrentChapter(dispatch, params);
        } else { // match '/space/book/:bookId'
            if (this.props.bookSpace.book.length === 0) {
                this.queryBooks(dispatch, params, null);
            }
            this.queryCurrentBook(dispatch, params,
                (res) => this.callback4Chapter(dispatch, res),
            );
        }
    }

    callback4Chapter = (dispatch, resp) => {
        if (resp.status === 'ok') {
            const {bookId, chapter} = resp.data;
            if (chapter && chapter.length > 0) {
                const {chapterId} = chapter[0];
                this.queryCurrentChapter(dispatch, {bookId, chapterId});
            } else {
                this.queryCurrentChapter(dispatch, {bookId, chapterId: ''});
            }
        }
    };

    queryBooks = (dispatch, params, callback) => {
        dispatch({
            type: 'bookSpace/fetchBook',
            payload: {
                userid: '001',
            },
            callback,
        });
    };

    queryCurrentBook = (dispatch, params, callback) => {
        dispatch({
            type: 'bookSpace/fetchCurrentBook',
            payload: {
                bookId: params.bookId,
                userid: '001',
            },

            callback,
        });
    };

    queryCurrentChapter = (dispatch, params) => {
        dispatch({
            type: 'bookSpace/fetchCurrentChapter',
            payload: {
                bookId: params.bookId,
                chapterId: params.chapterId,
                userid: '001',
            },
        });
    };


    resize = () => {
        if (!this.main) {
            return;
        }

        requestAnimationFrame(() => {
            if (!this.main) {
                return;
            }

            let mode = 'inline';
            const {offsetWidth} = this.main;

            if (this.main.offsetWidth < 641 && offsetWidth > 400) {
                mode = 'horizontal';
            }

            if (window.innerWidth < 768 && offsetWidth > 400) {
                mode = 'horizontal';
            }

            this.setState({
                mode,
            });
        });
    };

    addChapter = (currentBook) => {
        const { dispatch } = this.props;
        const { bookId } = currentBook;
        dispatch({
            type: 'bookSpace/addChapter',
            payload: {
                bookId,
                userid: '001',
            },
        });
    };

    render() {
        const {
            dispatch,
            bookSpace: {
                book,
                currentBook,
                currentChapter = {bookId: '', chapterId: '', title: '', content: ''}
            },
            match
        } = this.props;
        const {mode} = this.state;
        return (
{ if (ref) { this.main = ref; } }} >

内容查看此隐藏内容查看价格为99.79积分,请先
点击【立刻购买】给作者的精彩内容打赏一下!
(注*人民币1元充值可获得1积分)
export default connect(({bookSpace, loading}) => ({
    bookSpace,
    loading: loading.models.bookSpace,
}))(Book);

/*****************************booklist.jsx:***************************************/

import {Menu} from "antd";
import React from "react";
import {Link} from "umi";

const {Item} = Menu;


// react的精髓在于组件化,凡是运行时遵循事件驱动变化的部分,都应组件化,然后维护组件生命周期
// 组件化的中心思想是系统边界和时间边界,就是尽可能把发生在同一时间段内的网页元素组成一个组件,
// 能够影响一个组件状态变化的因素有两个,一个是前端用户的操作改变了组件的状态,另一个是触发与后端的交互,
// 来自后端的数据将更新组件的状态,这两个状态一个发生在组件与用户的界面上,另一个发生在组件与后端请求的界面上。
// 用户界面上发生的状态变化用前端state表达,这是由前端浏览器驱动和渲染的,与后端交互界面上发生的状态变化记录
// 在模型state里,由基于redux的dva框架托管,这两个state是异步的关系,他们只与其所属的组件发生直接关系,他们之间没有直接关联。
// 这样,组件初始化时的初始状态应该在组件内明确设定,如果初始状态来自前端,则从前端state给出默认值然后在return组件时引用,如果是来自后端,则必须从props中取出默认值,然后传给组件,不可以setstate然后取state。
// 根据以上,在划分组件时遵循:一个完整的用户交互界面只应该有一个顶级父组件,其余都是他的子组件,定义前端state和后端state的交互逻辑应该来自父组件,而子组件仅处理与本组件渲染有关的加工处理,其操作数据、操作方法等尽可能
// 引自父组件,父组件通过props传递必要的参数簇。这就是一个整体、多个局部的关系,从局部看,每个组件都是一个完整的组件,从整体看,又分为父子组件,父组件组织子组件在界面上的位置,子组件依赖于父组件的props
// 在整个组件发生交互重新渲染时,遵循同步规则和异步规则,有依赖关系的组件是同步渲染,无依赖关系的组件是异步渲染,同步渲染发生在父与子、级联组件之间,为了保持同步应该有一个出口处理与后台的交互,有一个入口处理用户的操作或输入,
// 为了同步执行,父调子或者子调父的方法时,都必须传递上文的dispatch到调用方法以触发dva执行下文对后台的访问,
// 默认在父组件定义和执行,然后子组件或者子级联组件仅接收变化的状态,或者通过组件引用ref的方式在上级组件触发事件时显式调用子组件的setstate更改其状态实现局部刷新。
// 事件驱动是组件渲染的起点,对一个界面发生的事件操作分为:用户界面操作(鼠标或键盘操作)和浏览器重新请求(刷新或重新请求),应该统一这两种操作的处理方法,主要是用统一的参数接收方法和统一的处理方法,后者属于组件初始化操作
// 如果是过程中发生的,则还需要保持已有的state状态(前后端均有),否则按初始化处理。
// 为了更好地适应pc seo,建议使用路由驱动传递参数,组件内部采用props.match接收参数。
内容查看此隐藏内容查看价格为99.79积分,请先
点击【立刻购买】给作者的精彩内容打赏一下!
(注*人民币1元充值可获得1积分)
export default BookList;

 /*******************chapter.jsx:*********************************/ 
import React from 'react';
import {Menu} from 'antd';
import {Link} from "umi";

const {Item} = Menu;

const Chapter = (props) => {
    const {
        dispatch,
        bookSpace: {currentBook, currentChapter},
        mode,
        queryBook,
    } = props;
    const {bookId, chapter} = currentBook;

    const getMenuChapter = (chapterList) => {
        return chapterList && chapterList.length > 0 ? chapterList.map((item) =>
                {item.title})
            : "";
    };
内容查看此隐藏内容查看价格为99.79积分,请先
点击【立刻购买】给作者的精彩内容打赏一下!
(注*人民币1元充值可获得1积分)
export default Chapter;

 /**********************base.jsx:********************************/ 

import React, { useState} from 'react';
import {Editor} from "@tinymce/tinymce-react";
import {Input} from "antd";

const BookView = (props) => {
    const {
        dispatch,
        bookSpace: { currentChapter },
    } = props;

    const [article, setArticle] = useState({title: '', content: ''});

    const {bookId, chapterId, title, content} = currentChapter;

    const handleEditorChange = (p) => {
        const {target: { innerHTML }} = p;
        setArticle({...article, content: innerHTML});
        // console.log('Content was updated: this.state.contentP', article.content);
    }
内容查看此隐藏内容查看价格为99.79积分,请先
点击【立刻购买】给作者的精彩内容打赏一下!
(注*人民币1元充值可获得1积分)
export default BookView; 

声明: 除非转自他站(如有侵权,请联系处理)外,本文采用 BY-NC-SA 协议进行授权 | 嗅谱网
转载请注明:转自《基于Ant design pro react 实现的动态路由跳转三级级联写作工作台源码
本文地址:http://www.xiupu.net/archives-10915.html
关注公众号:嗅谱网

赞赏

wechat pay微信赞赏alipay pay支付宝赞赏

上一篇
下一篇

相关文章

在线留言

你必须 登录后 才能留言!

  1. 星星

    主组件用到的样式index.less附上:
    @import ‘~antd/es/style/themes/default.less’;

    .main {
    display: flex;
    width: 100%;
    height: 100%;
    padding-top: 16px;
    //padding-bottom: 16px;
    overflow: hidden;
    background-color: @menu-bg;
    .leftMenu {
    width: 15%;
    min-width: 224px;
    border-right: @border-width-base @border-style-base @border-color-split;
    :global {
    .ant-menu-inline {
    border: none;
    }
    .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
    font-weight: bold;
    }
    }
    }
    .right {
    flex: 1;
    height: 100%;
    //padding-top: 8px;
    //padding-right: 40px;
    //padding-bottom: 8px;
    //padding-left: 40px;
    .title {
    margin-bottom: 12px;
    color: @heading-color;
    font-weight: 500;
    font-size: 20px;
    line-height: 28px;
    }
    }
    :global {
    .ant-list-split .ant-list-item:last-child {
    border-bottom: 1px solid @border-color-split;
    }
    .ant-list-item {
    padding-top: 14px;
    padding-bottom: 14px;
    }
    }
    }
    :global {
    .ant-list-item-meta {
    // 账号绑定图标
    .taobao {
    display: block;
    color: #ff4000;
    font-size: 48px;
    line-height: 48px;
    border-radius: @border-radius-base;
    }
    .dingding {
    margin: 2px;
    padding: 6px;
    color: #fff;
    font-size: 32px;
    line-height: 32px;
    background-color: #2eabff;
    border-radius: @border-radius-base;
    }
    .alipay {
    color: #2eabff;
    font-size: 48px;
    line-height: 48px;
    border-radius: @border-radius-base;
    }
    }

    // 密码强度
    font.strong {
    color: @success-color;
    }
    font.medium {
    color: @warning-color;
    }
    font.weak {
    color: @error-color;
    }
    }

    @media screen and (max-width: @screen-md) {
    .main {
    flex-direction: column;
    .leftMenu {
    width: 100%;
    border: none;
    }
    .right {
    padding: 40px;
    }
    }
    }