基于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; } }} >
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接收参数。
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}) : ""; };
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); }
export default BookView;
声明: 除非转自他站(如有侵权,请联系处理)外,本文采用 BY-NC-SA 协议进行授权 | 嗅谱网
转载请注明:转自《基于Ant design pro react 实现的动态路由跳转三级级联写作工作台源码》
本文地址:http://www.xiupu.net/archives-10915.html
关注公众号:
微信赞赏
支付宝赞赏
主组件用到的样式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;
}
}
}