commit
						cd82ac05e2
					
				@ -0,0 +1,116 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 评论表单
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-17 17:32:55
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-18 17:51:44
 | 
				
			||||
 */
 | 
				
			||||
import React, { useState } from 'react';
 | 
				
			||||
import { Form, Button, Input } from 'antd';
 | 
				
			||||
import QuillForEditor from '../../quillForEditor';
 | 
				
			||||
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html'
 | 
				
			||||
const FormItem = Form.Item;
 | 
				
			||||
 | 
				
			||||
function CommentForm (props) {
 | 
				
			||||
 | 
				
			||||
  const {
 | 
				
			||||
    commentCtxChagne,
 | 
				
			||||
    onCancel,
 | 
				
			||||
    onSubmit, 
 | 
				
			||||
    form
 | 
				
			||||
  } = props;
 | 
				
			||||
 | 
				
			||||
  const { getFieldDecorator } = form;
 | 
				
			||||
  const [ctx, setCtx] = useState('');
 | 
				
			||||
 | 
				
			||||
  const options = [
 | 
				
			||||
    ['bold', 'italic', 'underline'],
 | 
				
			||||
    [{header: [1,2,3,false]}],
 | 
				
			||||
    ['blockquote', 'code-block'],
 | 
				
			||||
    ['link', 'image'],
 | 
				
			||||
    ['formula']
 | 
				
			||||
  ];
 | 
				
			||||
  // const { form: { getFieldDecorator } } = props;
 | 
				
			||||
  const [showQuill, setShowQuill] = useState(false);
 | 
				
			||||
  // 点击输入框
 | 
				
			||||
  const handleInputClick = () => {
 | 
				
			||||
    setShowQuill(true);
 | 
				
			||||
  }
 | 
				
			||||
  // 取消
 | 
				
			||||
  const handleCancle = () => {
 | 
				
			||||
    setShowQuill(false);
 | 
				
			||||
    onCancel && onCancel();
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  // 编辑器内容变化时
 | 
				
			||||
  const handleContentChange = (content) => {
 | 
				
			||||
    setCtx(content);
 | 
				
			||||
    try {
 | 
				
			||||
      const _html = new QuillDeltaToHtmlConverter(content.ops, {}).convert();
 | 
				
			||||
      // props.form.setFieldsValue({'comment': _html.replace(/<\/?[^>]*>/g, '')});
 | 
				
			||||
      props.form.setFieldsValue({'comment': _html});
 | 
				
			||||
    } catch (error) {
 | 
				
			||||
      console.log(error);
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
  // 发送
 | 
				
			||||
  const handleSubmit = (e) => {
 | 
				
			||||
    e.preventDefault();
 | 
				
			||||
    props.form.validateFields((err, values) => {
 | 
				
			||||
      if (!err) {
 | 
				
			||||
        setShowQuill(false);
 | 
				
			||||
        const content = ctx;
 | 
				
			||||
        props.form.setFieldsValue({'comment': ''});
 | 
				
			||||
        setCtx('');
 | 
				
			||||
        console.log(content);
 | 
				
			||||
        onSubmit && onSubmit(content);
 | 
				
			||||
      }
 | 
				
			||||
    });
 | 
				
			||||
  }
 | 
				
			||||
  return (
 | 
				
			||||
    <Form>
 | 
				
			||||
      <FormItem>
 | 
				
			||||
        {
 | 
				
			||||
          getFieldDecorator('comment', {
 | 
				
			||||
            rules: [
 | 
				
			||||
              { required: true, message: '评论内容不能为空'}
 | 
				
			||||
            ],
 | 
				
			||||
          })(
 | 
				
			||||
            <Input 
 | 
				
			||||
              onClick={handleInputClick}
 | 
				
			||||
              placeholder="说点儿什么~"
 | 
				
			||||
              style={{
 | 
				
			||||
                height: showQuill ? '0px' : '40px',
 | 
				
			||||
                overflow: showQuill ? 'hidden' : 'auto',
 | 
				
			||||
                opacity: showQuill ? 0 : 1,
 | 
				
			||||
                transition: 'all .3s' 
 | 
				
			||||
              }}
 | 
				
			||||
            />
 | 
				
			||||
          )
 | 
				
			||||
        }
 | 
				
			||||
        
 | 
				
			||||
        <QuillForEditor
 | 
				
			||||
          imgAttrs={{width: '60px', height: '30px'}}
 | 
				
			||||
          wrapStyle={{
 | 
				
			||||
            height: showQuill ? 'auto' : '0px',
 | 
				
			||||
            opacity: showQuill ? 1 : 0,
 | 
				
			||||
            overflow: showQuill ? 'none' : 'hidden',
 | 
				
			||||
            transition: 'all 0.3s'
 | 
				
			||||
          }}
 | 
				
			||||
          style={{ height: '150px', overflowY: 'auto' }}
 | 
				
			||||
          placeholder="说点儿什么~"
 | 
				
			||||
          options={options}
 | 
				
			||||
          value={ctx}
 | 
				
			||||
          onContentChange={handleContentChange}
 | 
				
			||||
        />
 | 
				
			||||
      </FormItem>
 | 
				
			||||
      <FormItem style={{ textAlign: 'right' }}>
 | 
				
			||||
        <Button onClick={handleCancle}>取消</Button>
 | 
				
			||||
        <Button onClick={handleSubmit} type="primary" style={{ marginLeft: '10px'}}>发送</Button>
 | 
				
			||||
      </FormItem>
 | 
				
			||||
    </Form>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default Form.create()(CommentForm);
 | 
				
			||||
@ -0,0 +1,32 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-18 10:49:46
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-18 11:39:23
 | 
				
			||||
 */
 | 
				
			||||
import './index.scss';
 | 
				
			||||
import React from 'react';
 | 
				
			||||
import { Icon } from 'antd';
 | 
				
			||||
function CommentIcon ({
 | 
				
			||||
  type, // 图标类型
 | 
				
			||||
  count, // 评论数
 | 
				
			||||
  iconClick,
 | 
				
			||||
  ...props
 | 
				
			||||
}) {
 | 
				
			||||
 | 
				
			||||
  // 点击图标
 | 
				
			||||
  const handleSpanClick = () => {
 | 
				
			||||
    iconClick && iconClick();
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  return (
 | 
				
			||||
    <span className={`comment_icon_count ${props.className}`} onClick={ handleSpanClick }>
 | 
				
			||||
      <Icon className="comment_icon" type={type} />
 | 
				
			||||
      <span className="comment_count">{ count }</span>
 | 
				
			||||
    </span>
 | 
				
			||||
  )
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default CommentIcon;
 | 
				
			||||
@ -0,0 +1,165 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 评论单列
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-17 17:35:17
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-19 18:02:28
 | 
				
			||||
 */
 | 
				
			||||
import './index.scss';
 | 
				
			||||
import React, { useState } from 'react';
 | 
				
			||||
import CommentIcon from './CommentIcon';
 | 
				
			||||
import { getImageUrl, CNotificationHOC } from 'educoder'
 | 
				
			||||
import { Icon } from 'antd';
 | 
				
			||||
import moment from 'moment';
 | 
				
			||||
// import QuillForEditor from '../../quillForEditor';
 | 
				
			||||
import CommentForm from './CommentForm';
 | 
				
			||||
 | 
				
			||||
// import {ModalConfirm} from '../ModalConfirm';
 | 
				
			||||
function CommentItem ({
 | 
				
			||||
  options,
 | 
				
			||||
  confirm
 | 
				
			||||
}) {
 | 
				
			||||
  // 显示评论输入框
 | 
				
			||||
  const [showQuill, setShowQuill] = useState(false);
 | 
				
			||||
  // 加载更多评论内容
 | 
				
			||||
  const [showMore, setShowMore] = useState(false);
 | 
				
			||||
  // 箭头方向
 | 
				
			||||
  const [arrow, setArrow] = useState(false);
 | 
				
			||||
  // 删除评论
 | 
				
			||||
  const deleteComment = () => {
 | 
				
			||||
    console.log('删除评论...');
 | 
				
			||||
    confirm({
 | 
				
			||||
      title: '提示',
 | 
				
			||||
      content: (<p>确定要删除该条回复吗?</p>),
 | 
				
			||||
      onOk () {
 | 
				
			||||
        console.log('点击了删除');
 | 
				
			||||
      }
 | 
				
			||||
    });
 | 
				
			||||
    // ModalConfirm('提示', (<p>确定要删除该条回复吗?</p>), () => {
 | 
				
			||||
    //   console.log('点击了删除');
 | 
				
			||||
    // });
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  // 评论头像
 | 
				
			||||
  const commentAvatar = (url) => ( 
 | 
				
			||||
    <img className="item-flex flex-image" src='https://b-ssl.duitang.com/uploads/item/201511/13/20151113110434_kyReJ.jpeg' alt=""/>
 | 
				
			||||
  );
 | 
				
			||||
  
 | 
				
			||||
  // 评论信息
 | 
				
			||||
  const commentInfo = () => (
 | 
				
			||||
    <p className="item-header">
 | 
				
			||||
      <span className="item-name">用户名</span>
 | 
				
			||||
      <span className="item-time">{moment(new Date(), 'YYYYMMDD HHmmss').fromNow()}</span>
 | 
				
			||||
      <span className="item-close"><Icon type="close" onClick={deleteComment}/></span>
 | 
				
			||||
    </p>
 | 
				
			||||
  );
 | 
				
			||||
 | 
				
			||||
  // 评论内容
 | 
				
			||||
  const commentCtx = (ctx) => (
 | 
				
			||||
    <p className="item-ctx">
 | 
				
			||||
      这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容
 | 
				
			||||
    </p>
 | 
				
			||||
  );
 | 
				
			||||
 | 
				
			||||
  // 加载更多
 | 
				
			||||
  const handleOnLoadMore = () => {
 | 
				
			||||
    if (!arrow) {
 | 
				
			||||
      // 展开所有
 | 
				
			||||
    } else {
 | 
				
			||||
      // 收起
 | 
				
			||||
    }
 | 
				
			||||
    setArrow(!arrow);
 | 
				
			||||
  };
 | 
				
			||||
 | 
				
			||||
  // 评论追加内容
 | 
				
			||||
  const commentAppend = () => {
 | 
				
			||||
 | 
				
			||||
    return (
 | 
				
			||||
      <ul className="comment_item_append_list">
 | 
				
			||||
        <li className="comment_item_area">
 | 
				
			||||
          {commentAvatar()}
 | 
				
			||||
          <div className="item-flex item-desc">
 | 
				
			||||
            {commentInfo()}
 | 
				
			||||
            {commentCtx()}
 | 
				
			||||
          </div>
 | 
				
			||||
        </li>
 | 
				
			||||
        <li className="comment_item_area">
 | 
				
			||||
          {commentAvatar()}
 | 
				
			||||
          <div className="item-flex item-desc">
 | 
				
			||||
            {commentInfo()}
 | 
				
			||||
            {commentCtx()}
 | 
				
			||||
          </div>
 | 
				
			||||
        </li>
 | 
				
			||||
        <li className="comment_item_area">
 | 
				
			||||
          {commentAvatar()}
 | 
				
			||||
          <div className="item-flex item-desc">
 | 
				
			||||
            {commentInfo()}
 | 
				
			||||
            {commentCtx()}
 | 
				
			||||
          </div>
 | 
				
			||||
        </li>
 | 
				
			||||
 | 
				
			||||
        <li className="comment_item_loadmore" onClick={handleOnLoadMore}>
 | 
				
			||||
          <p className="loadmore-txt">展开其余23条评论</p>
 | 
				
			||||
          <p className="loadmore-icon">
 | 
				
			||||
            <Icon type={!arrow ? 'down' : 'up'}/>
 | 
				
			||||
          </p>
 | 
				
			||||
        </li>
 | 
				
			||||
      </ul>
 | 
				
			||||
    );
 | 
				
			||||
  };
 | 
				
			||||
  // 点击图标
 | 
				
			||||
  const handleIconClick = () => {}
 | 
				
			||||
  
 | 
				
			||||
  // 点击评论icon
 | 
				
			||||
  const handleClickMessage = () => {
 | 
				
			||||
    setShowQuill(true);
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  // 点击取消
 | 
				
			||||
  const handleClickCancel = () => {
 | 
				
			||||
    setShowQuill(false);
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  // 点击保存
 | 
				
			||||
  const handleClickSubmit = (content) => {
 | 
				
			||||
    // 保存并关闭
 | 
				
			||||
    setShowQuill(false);
 | 
				
			||||
    console.log('获取保存内容', content);
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  return (
 | 
				
			||||
    <li className="comment_item_area">
 | 
				
			||||
      {commentAvatar()}
 | 
				
			||||
      <div className="item-flex item-desc">
 | 
				
			||||
        {commentInfo()}
 | 
				
			||||
        {commentCtx()}
 | 
				
			||||
        
 | 
				
			||||
        {commentAppend()}
 | 
				
			||||
 | 
				
			||||
        <div className="comment_icon_area">
 | 
				
			||||
          <CommentIcon className='comment-icon-margin' type="eye" count="100" iconClick={handleIconClick}/>
 | 
				
			||||
          {/* 回复 */}
 | 
				
			||||
          <CommentIcon 
 | 
				
			||||
            className='comment-icon-margin' 
 | 
				
			||||
            type="message" count="100" 
 | 
				
			||||
            iconClick={handleClickMessage}
 | 
				
			||||
          />
 | 
				
			||||
          {/* 点赞 */}
 | 
				
			||||
          <CommentIcon/>
 | 
				
			||||
        </div>
 | 
				
			||||
 | 
				
			||||
        <div
 | 
				
			||||
          style={{ display: showQuill ? 'block' : 'none'}}
 | 
				
			||||
          className="comment_item_quill">
 | 
				
			||||
          <CommentForm 
 | 
				
			||||
            onCancel={handleClickCancel}
 | 
				
			||||
            onSubmit={handleClickSubmit}
 | 
				
			||||
          />
 | 
				
			||||
        </div>
 | 
				
			||||
      </div>
 | 
				
			||||
    </li>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default CNotificationHOC() (CommentItem);
 | 
				
			||||
@ -0,0 +1,20 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 评论列表页
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-17 17:34:00
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-18 11:48:09
 | 
				
			||||
 */
 | 
				
			||||
import './index.scss';
 | 
				
			||||
import React from 'react';
 | 
				
			||||
import CommentItem from './CommentItem';
 | 
				
			||||
function CommentList ({}) {
 | 
				
			||||
  return (
 | 
				
			||||
    <ul className="comment_list_wrapper">
 | 
				
			||||
      <CommentItem />
 | 
				
			||||
    </ul>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default CommentList;
 | 
				
			||||
@ -0,0 +1,22 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 评论组件
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-17 17:31:33
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-18 11:47:39
 | 
				
			||||
 */
 | 
				
			||||
import React from 'react';
 | 
				
			||||
import CommentForm from './CommentForm';
 | 
				
			||||
import CommentList from './CommentList';
 | 
				
			||||
function Comment (props) {
 | 
				
			||||
 | 
				
			||||
  return (
 | 
				
			||||
    <React.Fragment>
 | 
				
			||||
      <CommentForm />
 | 
				
			||||
      <CommentList />
 | 
				
			||||
    </React.Fragment>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default Comment;
 | 
				
			||||
@ -0,0 +1,111 @@
 | 
				
			||||
$bdColor: rgba(244,244,244,1);
 | 
				
			||||
$bgColor: rgba(250,250,250,1);
 | 
				
			||||
$lh14: 14px;
 | 
				
			||||
$lh22: 22px;
 | 
				
			||||
$fz14: 14px;
 | 
				
			||||
$fz12: 12px;
 | 
				
			||||
$ml: 20px;
 | 
				
			||||
 | 
				
			||||
.comment_list_wrapper{
 | 
				
			||||
  box-sizing: border-box;
 | 
				
			||||
  border-top: 1px solid $bdColor;
 | 
				
			||||
 | 
				
			||||
  .comment_item_area{
 | 
				
			||||
    display: flex;
 | 
				
			||||
    padding: 20px 0;
 | 
				
			||||
    box-sizing: border-box;
 | 
				
			||||
    border-bottom: 1px solid $bdColor;
 | 
				
			||||
    .flex-image{
 | 
				
			||||
      width: 48px;
 | 
				
			||||
      height: 48px;
 | 
				
			||||
      border-radius: 50%;
 | 
				
			||||
    }
 | 
				
			||||
    .item-desc{
 | 
				
			||||
      flex: 1;
 | 
				
			||||
      margin-left: $ml;
 | 
				
			||||
    }
 | 
				
			||||
    .item-header{
 | 
				
			||||
      font-size: $fz14;
 | 
				
			||||
      line-height: $lh14;
 | 
				
			||||
      color: #333;
 | 
				
			||||
      .item-time{
 | 
				
			||||
        font-size: $fz12;
 | 
				
			||||
        line-height: $lh14;
 | 
				
			||||
        margin-left: $ml;
 | 
				
			||||
      }
 | 
				
			||||
      .item-close{
 | 
				
			||||
        float: right;
 | 
				
			||||
        cursor: pointer;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
    .item-ctx{
 | 
				
			||||
      line-height: $lh22;
 | 
				
			||||
      font-size: $fz12;
 | 
				
			||||
      color: #333;
 | 
				
			||||
      margin-top: 10px;
 | 
				
			||||
    }
 | 
				
			||||
    .comment_icon_area{
 | 
				
			||||
      display: flex;
 | 
				
			||||
      justify-content: flex-end;
 | 
				
			||||
      margin-top: 10px;
 | 
				
			||||
 | 
				
			||||
      .comment-icon-margin{
 | 
				
			||||
        margin-left: 30px;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    .comment_item_quill{
 | 
				
			||||
      margin-top: 20px;
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
  .comment_icon_count{
 | 
				
			||||
    cursor: pointer;
 | 
				
			||||
    font-size: 12px;
 | 
				
			||||
    line-height: 1.5;
 | 
				
			||||
 | 
				
			||||
    .comment_icon{
 | 
				
			||||
      color: #333;
 | 
				
			||||
    }
 | 
				
			||||
    .comment_count{
 | 
				
			||||
      color: #999999;
 | 
				
			||||
      margin-left: 10px;
 | 
				
			||||
      transition: color .3s;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    &:hover{
 | 
				
			||||
      .comment_icon,
 | 
				
			||||
      .comment_count{
 | 
				
			||||
        color: #5091FF;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
  .comment_item_append_list{
 | 
				
			||||
    position: relative;
 | 
				
			||||
    background-color: $bgColor;
 | 
				
			||||
    border-radius: 5px;
 | 
				
			||||
    padding: 0 15px 10px;
 | 
				
			||||
    margin: 15px 0;
 | 
				
			||||
    &::before {
 | 
				
			||||
      position: absolute;
 | 
				
			||||
      left: 15px;
 | 
				
			||||
      bottom: 100%;
 | 
				
			||||
      height: 0;
 | 
				
			||||
      width: 0;
 | 
				
			||||
      content: '';
 | 
				
			||||
      // border: 5px solid transparent;
 | 
				
			||||
      border: 10px solid transparent;
 | 
				
			||||
      border-bottom-color: $bgColor;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    .comment_item_loadmore{
 | 
				
			||||
      padding-top: 10px;
 | 
				
			||||
      cursor: pointer;
 | 
				
			||||
      .loadmore-txt,
 | 
				
			||||
      .loadmore-icon{
 | 
				
			||||
        color: #999;
 | 
				
			||||
        text-align: center;
 | 
				
			||||
        font-size: $fz12;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,54 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 重写图片
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-16 15:50:45
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-17 16:44:48
 | 
				
			||||
 */
 | 
				
			||||
import Quill from "quill";
 | 
				
			||||
 | 
				
			||||
const BlockEmbed = Quill.import('blots/block/embed');
 | 
				
			||||
 | 
				
			||||
export default class ImageBlot extends BlockEmbed {
 | 
				
			||||
 | 
				
			||||
  static create(value) {
 | 
				
			||||
 | 
				
			||||
    const node = super.create();
 | 
				
			||||
 | 
				
			||||
    node.setAttribute('alt', value.alt);
 | 
				
			||||
    node.setAttribute('src', value.url);
 | 
				
			||||
    
 | 
				
			||||
    if (value.width) {
 | 
				
			||||
      node.setAttribute('width', value.width);
 | 
				
			||||
    }
 | 
				
			||||
    if (value.height) {
 | 
				
			||||
      node.setAttribute('height', value.height);
 | 
				
			||||
    }
 | 
				
			||||
    // 宽度和高度都不存在时,
 | 
				
			||||
    if (!value.width && !value.height) {
 | 
				
			||||
      node.setAttribute('display', 'block');
 | 
				
			||||
      node.setAttribute('width', '100%');
 | 
				
			||||
    }
 | 
				
			||||
    // 给图片添加点击事件
 | 
				
			||||
    node.onclick = () => {
 | 
				
			||||
      value.onClick && value.onClick(value.url);
 | 
				
			||||
    }
 | 
				
			||||
    return node;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  static value (node) {
 | 
				
			||||
 | 
				
			||||
    return {
 | 
				
			||||
      alt: node.getAttribute('alt'),
 | 
				
			||||
      url: node.getAttribute('src'),
 | 
				
			||||
      onclick: node.onclick,
 | 
				
			||||
      // width: node.width,
 | 
				
			||||
      // height: node.height,
 | 
				
			||||
      display: node.getAttribute('display')
 | 
				
			||||
    };
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
ImageBlot.blotName = 'image';
 | 
				
			||||
ImageBlot.tagName = 'img';
 | 
				
			||||
@ -0,0 +1,47 @@
 | 
				
			||||
function deepEqual (prev, current) {
 | 
				
			||||
  if (prev === current) { // 基本类型比较,值,类型都相同 或者同为 null or undefined
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if ((!prev && current) 
 | 
				
			||||
    || (prev && !current)
 | 
				
			||||
    || (!prev && !current)
 | 
				
			||||
  ) {
 | 
				
			||||
    return false;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if (Array.isArray(prev)) {
 | 
				
			||||
    if (!Array.isArray(current)) return false;
 | 
				
			||||
    if (prev.length !== current.length) return false;
 | 
				
			||||
    
 | 
				
			||||
    for (let i = 0; i < prev.length; i++) {
 | 
				
			||||
      if (!deepEqual(current[i], prev[i])) {
 | 
				
			||||
        return false;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if (typeof current === 'object') {
 | 
				
			||||
    if (typeof prev !== 'object') return false;
 | 
				
			||||
    const prevKeys = Object.keys(prev);
 | 
				
			||||
    const curKeys = Object.keys(current);
 | 
				
			||||
 | 
				
			||||
    if (prevKeys.length !== curKeys.length) return false;
 | 
				
			||||
 | 
				
			||||
    prevKeys.sort();
 | 
				
			||||
    curKeys.sort();
 | 
				
			||||
 | 
				
			||||
    for (let i = 0; i < prevKeys.length; i++) {
 | 
				
			||||
      if (prevKeys[i] !== curKeys[i]) return false;
 | 
				
			||||
      const key = prevKeys[i];
 | 
				
			||||
      if (!deepEqual(prev[key], current[key])) return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  return false;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default deepEqual;
 | 
				
			||||
@ -0,0 +1,166 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: quill 编辑器
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-18 08:49:30
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-19 16:58:50
 | 
				
			||||
 */
 | 
				
			||||
import 'quill/dist/quill.core.css'; // 核心样式
 | 
				
			||||
import 'quill/dist/quill.snow.css'; // 有工具栏
 | 
				
			||||
import 'quill/dist/quill.bubble.css'; // 无工具栏
 | 
				
			||||
import 'katex/dist/katex.min.css'; // katex 表达式样式
 | 
				
			||||
import React, { useState, useRef, useEffect } from 'react';
 | 
				
			||||
import Quill from 'quill';
 | 
				
			||||
import katex from 'katex';
 | 
				
			||||
import deepEqual from './deepEqual.js'
 | 
				
			||||
import { fetchUploadImage } from '../../services/ojService.js';
 | 
				
			||||
import { getImageUrl } from 'educoder'
 | 
				
			||||
import ImageBlot from './ImageBlot';
 | 
				
			||||
 | 
				
			||||
window.Quill = Quill;
 | 
				
			||||
window.katex = katex;
 | 
				
			||||
Quill.register(ImageBlot);
 | 
				
			||||
 | 
				
			||||
function QuillForEditor ({
 | 
				
			||||
  placeholder,
 | 
				
			||||
  readOnly,
 | 
				
			||||
  options,
 | 
				
			||||
  value,
 | 
				
			||||
  imgAttrs = {}, // 指定图片的宽高
 | 
				
			||||
  style = {},
 | 
				
			||||
  wrapStyle = {},
 | 
				
			||||
  showUploadImage,
 | 
				
			||||
  onContentChange
 | 
				
			||||
}) {
 | 
				
			||||
  // toolbar 默认值
 | 
				
			||||
  const defaultConfig = [
 | 
				
			||||
    ['bold', 'italic', 'underline'],
 | 
				
			||||
    [{align: []}, {list: 'ordered'}, {list: 'bullet'}], // 列表
 | 
				
			||||
    [{script: 'sub'}, {script: 'super'}],
 | 
				
			||||
    [{ 'color': [] }, { 'background': [] }], 
 | 
				
			||||
    [{header: [1,2,3,4,5,false]}],
 | 
				
			||||
    ['blockquote', 'code-block'],
 | 
				
			||||
    ['link', 'image', 'video'],
 | 
				
			||||
    ['formula'],
 | 
				
			||||
    ['clean']
 | 
				
			||||
  ];
 | 
				
			||||
 | 
				
			||||
  const editorRef = useRef(null);
 | 
				
			||||
  // quill 实例
 | 
				
			||||
  const [quill, setQuill] = useState(null);  
 | 
				
			||||
  const [selection, setSelection] = useState(null);
 | 
				
			||||
 | 
				
			||||
  // 文本内容变化时
 | 
				
			||||
  const handleOnChange = content => {
 | 
				
			||||
    // console.log('编辑器内容====》》》》', content);
 | 
				
			||||
    onContentChange && onContentChange(content);
 | 
				
			||||
  };
 | 
				
			||||
  
 | 
				
			||||
  const renderOptions = options || defaultConfig;
 | 
				
			||||
  // quill 配置信息
 | 
				
			||||
  const quillOption = {
 | 
				
			||||
    modules: {
 | 
				
			||||
      toolbar: renderOptions
 | 
				
			||||
    },
 | 
				
			||||
    readOnly,
 | 
				
			||||
    placeholder,
 | 
				
			||||
    theme: readOnly ? 'bubble' : 'snow'
 | 
				
			||||
  };
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    const _quill = new Quill(editorRef.current, quillOption);
 | 
				
			||||
    setQuill(_quill);
 | 
				
			||||
 | 
				
			||||
    // 处理图片上传功能
 | 
				
			||||
    _quill.getModule('toolbar').addHandler('image', (e) => {
 | 
				
			||||
      const input = document.createElement('input');
 | 
				
			||||
      input.setAttribute('type', 'file');
 | 
				
			||||
      input.setAttribute('accept', 'image/*');
 | 
				
			||||
      input.click();
 | 
				
			||||
 | 
				
			||||
      input.onchange = async (e) => {
 | 
				
			||||
        const file = input.files[0]; // 获取文件信息
 | 
				
			||||
        const formData = new FormData();
 | 
				
			||||
        formData.append('file', file);
 | 
				
			||||
 | 
				
			||||
        const range = _quill.getSelection(true);
 | 
				
			||||
        let fileUrl = ''; // 保存上传成功后图片的url
 | 
				
			||||
        // 上传文件
 | 
				
			||||
        const result = await fetchUploadImage(formData);
 | 
				
			||||
        // 获取上传图片的url
 | 
				
			||||
        if (result.data && result.data.id) {
 | 
				
			||||
          fileUrl = getImageUrl(`api/attachments/${result.data.id}`);
 | 
				
			||||
        }
 | 
				
			||||
        // 根据id获取文件路径
 | 
				
			||||
        const { width, height } = imgAttrs;
 | 
				
			||||
        // console.log('上传图片的url:', fileUrl);
 | 
				
			||||
        if (fileUrl) {
 | 
				
			||||
          _quill.insertEmbed(range.index, 'image', {
 | 
				
			||||
            url: fileUrl,
 | 
				
			||||
            alt: '图片信息',
 | 
				
			||||
            onClick: showUploadImage,
 | 
				
			||||
            width,
 | 
				
			||||
            height
 | 
				
			||||
          }); 
 | 
				
			||||
        }
 | 
				
			||||
      }
 | 
				
			||||
    });
 | 
				
			||||
  }, []);
 | 
				
			||||
  
 | 
				
			||||
  // 设置值 
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!quill) return
 | 
				
			||||
    const previous = quill.getContents()
 | 
				
			||||
    const current = value
 | 
				
			||||
 | 
				
			||||
    if (!deepEqual(previous, current)) {
 | 
				
			||||
      setSelection(quill.getSelection())
 | 
				
			||||
      if (typeof value === 'string') {
 | 
				
			||||
        quill.clipboard.dangerouslyPasteHTML(value, 'api')
 | 
				
			||||
      } else {
 | 
				
			||||
        quill.setContents(value)
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, value, setQuill]);
 | 
				
			||||
 | 
				
			||||
  // 清除选择区域
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (quill && selection) {
 | 
				
			||||
      quill.setSelection(selection)
 | 
				
			||||
      setSelection(null)
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, selection, setSelection]);
 | 
				
			||||
 | 
				
			||||
  // 设置placeholder值
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!quill || !quill.root) return;
 | 
				
			||||
    quill.root.dataset.placeholder = placeholder;
 | 
				
			||||
  }, [quill, placeholder]);
 | 
				
			||||
 | 
				
			||||
  // 处理内容变化
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!quill) return;
 | 
				
			||||
    if (typeof handleOnChange !== 'function') return;
 | 
				
			||||
    let handler;
 | 
				
			||||
    quill.on(
 | 
				
			||||
      'text-change', 
 | 
				
			||||
      (handler = () => {
 | 
				
			||||
        handleOnChange(quill.getContents()); // getContents: 检索编辑器内容
 | 
				
			||||
      })
 | 
				
			||||
    );
 | 
				
			||||
    return () => {
 | 
				
			||||
      quill.off('text-change', handler);
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, handleOnChange]);
 | 
				
			||||
 | 
				
			||||
  // 返回结果
 | 
				
			||||
  return (
 | 
				
			||||
    <div className='quill_editor_for_react_area' style={wrapStyle}>
 | 
				
			||||
      <div ref={editorRef} style={style}></div>
 | 
				
			||||
    </div>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default QuillForEditor;
 | 
				
			||||
@ -0,0 +1,54 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 重写图片
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-16 15:50:45
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-17 16:44:48
 | 
				
			||||
 */
 | 
				
			||||
import Quill from "quill";
 | 
				
			||||
 | 
				
			||||
const BlockEmbed = Quill.import('blots/block/embed');
 | 
				
			||||
 | 
				
			||||
export default class ImageBlot extends BlockEmbed {
 | 
				
			||||
 | 
				
			||||
  static create(value) {
 | 
				
			||||
 | 
				
			||||
    const node = super.create();
 | 
				
			||||
 | 
				
			||||
    node.setAttribute('alt', value.alt);
 | 
				
			||||
    node.setAttribute('src', value.url);
 | 
				
			||||
    
 | 
				
			||||
    if (value.width) {
 | 
				
			||||
      node.setAttribute('width', value.width);
 | 
				
			||||
    }
 | 
				
			||||
    if (value.height) {
 | 
				
			||||
      node.setAttribute('height', value.height);
 | 
				
			||||
    }
 | 
				
			||||
    // 宽度和高度都不存在时,
 | 
				
			||||
    if (!value.width && !value.height) {
 | 
				
			||||
      node.setAttribute('display', 'block');
 | 
				
			||||
      node.setAttribute('width', '100%');
 | 
				
			||||
    }
 | 
				
			||||
    // 给图片添加点击事件
 | 
				
			||||
    node.onclick = () => {
 | 
				
			||||
      value.onClick && value.onClick(value.url);
 | 
				
			||||
    }
 | 
				
			||||
    return node;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  static value (node) {
 | 
				
			||||
 | 
				
			||||
    return {
 | 
				
			||||
      alt: node.getAttribute('alt'),
 | 
				
			||||
      url: node.getAttribute('src'),
 | 
				
			||||
      onclick: node.onclick,
 | 
				
			||||
      // width: node.width,
 | 
				
			||||
      // height: node.height,
 | 
				
			||||
      display: node.getAttribute('display')
 | 
				
			||||
    };
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
ImageBlot.blotName = 'image';
 | 
				
			||||
ImageBlot.tagName = 'img';
 | 
				
			||||
@ -0,0 +1,45 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:09:42
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-18 08:46:20
 | 
				
			||||
 */
 | 
				
			||||
import 'quill/dist/quill.core.css'; // 核心样式
 | 
				
			||||
import 'quill/dist/quill.snow.css'; // 有工具栏
 | 
				
			||||
import 'quill/dist/quill.bubble.css'; // 无工具栏
 | 
				
			||||
import 'katex/dist/katex.min.css'; // katex 表达式样式
 | 
				
			||||
import React, { useState, useReducer, useEffect } from 'react';
 | 
				
			||||
import useQuill from './useQuill';
 | 
				
			||||
 | 
				
			||||
function ReactQuill ({
 | 
				
			||||
  disallowColors,  // 不可见时颜色
 | 
				
			||||
  placeholder, // 提示信息
 | 
				
			||||
  uploadImage, // 图片上传
 | 
				
			||||
  onChange, // 内容变化时
 | 
				
			||||
  options, // 配置信息 
 | 
				
			||||
  value, // 显示的内容
 | 
				
			||||
  style,
 | 
				
			||||
  showUploadImage // 显示上传图片
 | 
				
			||||
}) {
 | 
				
			||||
 | 
				
			||||
  const [element, setElement] = useState(); // quill 渲染节点
 | 
				
			||||
 | 
				
			||||
  useQuill({
 | 
				
			||||
    disallowColors,
 | 
				
			||||
    placeholder,
 | 
				
			||||
    uploadImage,
 | 
				
			||||
    onChange,
 | 
				
			||||
    options,
 | 
				
			||||
    value,
 | 
				
			||||
    showUploadImage,
 | 
				
			||||
    element
 | 
				
			||||
  });
 | 
				
			||||
 | 
				
			||||
  return (
 | 
				
			||||
    <div className='react_quill_area' ref={setElement} style={style}/>
 | 
				
			||||
  );
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default ReactQuill;
 | 
				
			||||
@ -0,0 +1,47 @@
 | 
				
			||||
function deepEqual (prev, current) {
 | 
				
			||||
  if (prev === current) { // 基本类型比较,值,类型都相同 或者同为 null or undefined
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if ((!prev && current) 
 | 
				
			||||
    || (prev && !current)
 | 
				
			||||
    || (!prev && !current)
 | 
				
			||||
  ) {
 | 
				
			||||
    return false;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if (Array.isArray(prev)) {
 | 
				
			||||
    if (!Array.isArray(current)) return false;
 | 
				
			||||
    if (prev.length !== current.length) return false;
 | 
				
			||||
    
 | 
				
			||||
    for (let i = 0; i < prev.length; i++) {
 | 
				
			||||
      if (!deepEqual(current[i], prev[i])) {
 | 
				
			||||
        return false;
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  if (typeof current === 'object') {
 | 
				
			||||
    if (typeof prev !== 'object') return false;
 | 
				
			||||
    const prevKeys = Object.keys(prev);
 | 
				
			||||
    const curKeys = Object.keys(current);
 | 
				
			||||
 | 
				
			||||
    if (prevKeys.length !== curKeys.length) return false;
 | 
				
			||||
 | 
				
			||||
    prevKeys.sort();
 | 
				
			||||
    curKeys.sort();
 | 
				
			||||
 | 
				
			||||
    for (let i = 0; i < prevKeys.length; i++) {
 | 
				
			||||
      if (prevKeys[i] !== curKeys[i]) return false;
 | 
				
			||||
      const key = prevKeys[i];
 | 
				
			||||
      if (!deepEqual(prev[key], current[key])) return false;
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    return true;
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  return false;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default deepEqual;
 | 
				
			||||
@ -0,0 +1,26 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 将多维数组转变成一维数组
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:35:01
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-16 11:36:22
 | 
				
			||||
 */
 | 
				
			||||
function flatten (array) {
 | 
				
			||||
  return flatten.rec(array, []);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
flatten.rec = function flatten (array, result) {
 | 
				
			||||
 | 
				
			||||
  for (let item of array) {
 | 
				
			||||
    if (Array.isArray(item)) {
 | 
				
			||||
      flatten(item, result);
 | 
				
			||||
    } else {
 | 
				
			||||
      result.push(item);
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
 | 
				
			||||
  return result;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default flatten;
 | 
				
			||||
@ -0,0 +1,32 @@
 | 
				
			||||
#quill-toolbar{
 | 
				
			||||
  .quill-btn{
 | 
				
			||||
    vertical-align: middle;
 | 
				
			||||
  }
 | 
				
			||||
  .quill_image{
 | 
				
			||||
    display: inline-block;
 | 
				
			||||
    position: relative;
 | 
				
			||||
    vertical-align: middle;
 | 
				
			||||
    width: 28px;
 | 
				
			||||
    height: 24px;
 | 
				
			||||
    overflow: hidden;
 | 
				
			||||
    .image_input{
 | 
				
			||||
      position: absolute;
 | 
				
			||||
      left: 0;
 | 
				
			||||
      right: 0;
 | 
				
			||||
      top: 0;
 | 
				
			||||
      bottom: 0;
 | 
				
			||||
      opacity: 0;
 | 
				
			||||
    }
 | 
				
			||||
    .ql-image{
 | 
				
			||||
      position: relative;
 | 
				
			||||
      left: 0;
 | 
				
			||||
      top: 0;
 | 
				
			||||
    }
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
.react_quill_area{
 | 
				
			||||
  .ql-toolbar:not(:last-child) {
 | 
				
			||||
    display: none;
 | 
				
			||||
  }
 | 
				
			||||
}
 | 
				
			||||
@ -0,0 +1,13 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 导出 ReactQuill
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:08:24
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-16 11:37:13
 | 
				
			||||
 */
 | 
				
			||||
import ReactQuill from './ReactQuill';
 | 
				
			||||
import useQuill from './useQuill';
 | 
				
			||||
 | 
				
			||||
export default ReactQuill;
 | 
				
			||||
export { useQuill };
 | 
				
			||||
@ -0,0 +1,27 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-12 19:48:55
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-16 11:38:16
 | 
				
			||||
 */
 | 
				
			||||
import { useState, useEffect } from 'react';
 | 
				
			||||
import deepEqual from './deepEqual';
 | 
				
			||||
 | 
				
			||||
function useDeepEqual (input) {
 | 
				
			||||
 | 
				
			||||
  const [value, setValue] = useState(input);
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
 | 
				
			||||
    if (!deepEqual(input, value)) {
 | 
				
			||||
      setValue(input)
 | 
				
			||||
    }
 | 
				
			||||
    
 | 
				
			||||
  }, [input, value]);
 | 
				
			||||
 | 
				
			||||
  return value;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useDeepEqual;
 | 
				
			||||
@ -0,0 +1,148 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 创建 reactQuill实例
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:31:42
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-17 20:42:05
 | 
				
			||||
 */
 | 
				
			||||
import Quill from 'quill'; // 导入quill
 | 
				
			||||
import { useState, useEffect, useMemo } from 'react';
 | 
				
			||||
import flatten from './flatten.js';
 | 
				
			||||
import useDeepEqualMemo from './useDeepEqualMemo';
 | 
				
			||||
import Katex from 'katex';
 | 
				
			||||
import ImageBlot from './ImageBlot';
 | 
				
			||||
import { fetchUploadImage } from '../../services/ojService.js';
 | 
				
			||||
import { getImageUrl } from 'educoder'
 | 
				
			||||
window.katex = Katex;
 | 
				
			||||
 | 
				
			||||
Quill.register(ImageBlot);
 | 
				
			||||
 | 
				
			||||
function useMountQuill ({
 | 
				
			||||
  element,
 | 
				
			||||
  options: passedOptions,
 | 
				
			||||
  uploadImage,
 | 
				
			||||
  showUploadImage,
 | 
				
			||||
  imgAttrs = {} // 指定图片的宽高属性
 | 
				
			||||
}) {
 | 
				
			||||
 | 
				
			||||
  // 是否引入 katex
 | 
				
			||||
  const [katexLoaded, setKatexLoaded] = useState(Boolean(window.katex))
 | 
				
			||||
  const [quill, setQuill] = useState(null);
 | 
				
			||||
 | 
				
			||||
  const options = useDeepEqualMemo(passedOptions);
 | 
				
			||||
  console.log('use mount quill: ', passedOptions);
 | 
				
			||||
 | 
				
			||||
  // 判断options中是否包含公式
 | 
				
			||||
  const requireKatex = useMemo(() => {
 | 
				
			||||
    return flatten(options.modules.toolbar).includes('formula');
 | 
				
			||||
  }, [options]);
 | 
				
			||||
 | 
				
			||||
  // 加载katex
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!requireKatex) return;
 | 
				
			||||
    if (katexLoaded) return;
 | 
				
			||||
 | 
				
			||||
    const interval = setInterval(() => {
 | 
				
			||||
      if (window.katex) {
 | 
				
			||||
        setKatexLoaded(true);
 | 
				
			||||
        clearInterval(interval); 
 | 
				
			||||
      }
 | 
				
			||||
    });
 | 
				
			||||
 | 
				
			||||
    return () => { // 定义回调清除定时器
 | 
				
			||||
      clearInterval(interval);
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
  }, [
 | 
				
			||||
    setKatexLoaded,
 | 
				
			||||
    katexLoaded,
 | 
				
			||||
    requireKatex
 | 
				
			||||
  ]);
 | 
				
			||||
 | 
				
			||||
  // 加载 quill
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!element) return;
 | 
				
			||||
    if (requireKatex && !katexLoaded) {
 | 
				
			||||
      element.innerHTML = `
 | 
				
			||||
        <div style="color: #ddd">
 | 
				
			||||
          Loading Katex...
 | 
				
			||||
        </div>
 | 
				
			||||
      `
 | 
				
			||||
    }
 | 
				
			||||
    // 清空内容
 | 
				
			||||
    element.innerHTML = '';
 | 
				
			||||
    console.log(element);
 | 
				
			||||
    // 创建 quill 节点
 | 
				
			||||
    const quillNode = document.createElement('div');
 | 
				
			||||
    element.appendChild(quillNode);  // 将quill节点追回到 element 元素中
 | 
				
			||||
 | 
				
			||||
    const quill = new Quill(element, options);
 | 
				
			||||
    setQuill(quill);
 | 
				
			||||
    // 加载上传图片功能
 | 
				
			||||
    if (typeof uploadImage === 'function') {
 | 
				
			||||
      quill.getModule('toolbar').addHandler('image', (e) => {
 | 
				
			||||
        // 创建type类型输入框加载本地图片
 | 
				
			||||
        const input = document.createElement('input');
 | 
				
			||||
        input.setAttribute('type', 'file');
 | 
				
			||||
        input.setAttribute('accept', 'image/*');
 | 
				
			||||
        input.click();
 | 
				
			||||
 | 
				
			||||
        input.onchange = async (e) => {
 | 
				
			||||
          const file = input.files[0]; // 获取文件信息
 | 
				
			||||
          const formData = new FormData();
 | 
				
			||||
          formData.append('file', file);
 | 
				
			||||
 | 
				
			||||
          // const reader = new FileReader();
 | 
				
			||||
          // reader.readAsDataURL(file);
 | 
				
			||||
          // console.log('文件信息===>>', reader);
 | 
				
			||||
          // reader.onload = function (e) {
 | 
				
			||||
          //   debugger;
 | 
				
			||||
          //   console.log('文件信息===>>', e.target.result);
 | 
				
			||||
          //   const image = new Image();
 | 
				
			||||
          //   image.src = e.target.result;
 | 
				
			||||
 | 
				
			||||
          //   image.onload = function () {
 | 
				
			||||
          //     // file.width = 
 | 
				
			||||
          //     console.log(image.width, image.height);
 | 
				
			||||
          //   }
 | 
				
			||||
          // }
 | 
				
			||||
 | 
				
			||||
          const range = quill.getSelection(true);
 | 
				
			||||
          let fileUrl = ''; // 保存上传成功后图片的url
 | 
				
			||||
          // 上传文件
 | 
				
			||||
          const result = await fetchUploadImage(formData);
 | 
				
			||||
          // 获取上传图片的url
 | 
				
			||||
          if (result.data && result.data.id) {
 | 
				
			||||
            fileUrl = getImageUrl(`api/attachments/${result.data.id}`);
 | 
				
			||||
          }
 | 
				
			||||
          // 根据id获取文件路径
 | 
				
			||||
          const { width, height } = imgAttrs;
 | 
				
			||||
          // console.log('上传图片的url:', fileUrl);
 | 
				
			||||
          if (fileUrl) {
 | 
				
			||||
            quill.insertEmbed(range.index, 'image', {
 | 
				
			||||
              url: fileUrl,
 | 
				
			||||
              alt: '',
 | 
				
			||||
              onClick: showUploadImage,
 | 
				
			||||
              width,
 | 
				
			||||
              height
 | 
				
			||||
            }); 
 | 
				
			||||
          }
 | 
				
			||||
        }
 | 
				
			||||
      });
 | 
				
			||||
    }
 | 
				
			||||
 | 
				
			||||
    return () => {
 | 
				
			||||
      element.innerHTML = '';
 | 
				
			||||
    }
 | 
				
			||||
  }, [
 | 
				
			||||
    element,
 | 
				
			||||
    options,
 | 
				
			||||
    requireKatex,
 | 
				
			||||
    katexLoaded,
 | 
				
			||||
  ]);
 | 
				
			||||
 | 
				
			||||
  return quill;
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useMountQuill;
 | 
				
			||||
@ -0,0 +1,60 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:09:50
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-17 15:46:50
 | 
				
			||||
 */
 | 
				
			||||
import useQuillPlaceholder from './useQuillPlaceholder';
 | 
				
			||||
import useQuillValueSync from './useQuillValueSync';
 | 
				
			||||
import useQuillOnChange from './useQuillOnChange';
 | 
				
			||||
import useMountQuill from './useMountQuill';
 | 
				
			||||
import { useEffect } from 'react';
 | 
				
			||||
 | 
				
			||||
function useQuill ({
 | 
				
			||||
  disallowColors,
 | 
				
			||||
  placeholder,
 | 
				
			||||
  uploadImage,
 | 
				
			||||
  onChange,
 | 
				
			||||
  options,
 | 
				
			||||
  value,
 | 
				
			||||
  element,
 | 
				
			||||
  showUploadImage
 | 
				
			||||
}) {
 | 
				
			||||
 | 
				
			||||
  // 获取 quill 实例
 | 
				
			||||
  const quill = useMountQuill({
 | 
				
			||||
    element,
 | 
				
			||||
    options,
 | 
				
			||||
    uploadImage,
 | 
				
			||||
    showUploadImage
 | 
				
			||||
  });
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (disallowColors && quill) {
 | 
				
			||||
      quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
 | 
				
			||||
        delta.ops = delta.ops.map(op => {
 | 
				
			||||
          if (op.attributes && op.attributes.color) {
 | 
				
			||||
            const { color, ...attributes } = op.attributes;
 | 
				
			||||
            return {
 | 
				
			||||
              ...op,
 | 
				
			||||
              attributes
 | 
				
			||||
            }
 | 
				
			||||
          }
 | 
				
			||||
          return op;
 | 
				
			||||
        });
 | 
				
			||||
        return delta;
 | 
				
			||||
      });
 | 
				
			||||
    }
 | 
				
			||||
  }, [
 | 
				
			||||
    disallowColors,
 | 
				
			||||
    quill
 | 
				
			||||
  ]);
 | 
				
			||||
  
 | 
				
			||||
  useQuillPlaceholder(quill, placeholder);
 | 
				
			||||
  useQuillValueSync(quill, value);
 | 
				
			||||
  useQuillOnChange(quill, onChange);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useQuill;
 | 
				
			||||
@ -0,0 +1,33 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-12 19:49:11
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-16 11:39:27
 | 
				
			||||
 */
 | 
				
			||||
import { useEffect } from 'react';
 | 
				
			||||
 | 
				
			||||
function useQuillOnChange (quill, onChange) {
 | 
				
			||||
  
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
 | 
				
			||||
    if (!quill) return;
 | 
				
			||||
    if (typeof onChange !== 'function') return;
 | 
				
			||||
 | 
				
			||||
    let handler;
 | 
				
			||||
 | 
				
			||||
    quill.on(
 | 
				
			||||
      'text-change', 
 | 
				
			||||
      (handler = () => {
 | 
				
			||||
        onChange(quill.getContents()); // getContents: 检索编辑器内容
 | 
				
			||||
      })
 | 
				
			||||
    );
 | 
				
			||||
 | 
				
			||||
    return () => {
 | 
				
			||||
      quill.off('text-change', handler);
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, onChange]);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useQuillOnChange;
 | 
				
			||||
@ -0,0 +1,22 @@
 | 
				
			||||
/*
 | 
				
			||||
 * @Description: 
 | 
				
			||||
 * @Author: tangjiang
 | 
				
			||||
 * @Github: 
 | 
				
			||||
 * @Date: 2019-12-09 09:28:34
 | 
				
			||||
 * @LastEditors: tangjiang
 | 
				
			||||
 * @LastEditTime: 2019-12-16 11:39:48
 | 
				
			||||
 */
 | 
				
			||||
import { useEffect } from 'react'
 | 
				
			||||
 | 
				
			||||
function useQuillPlaceholder (
 | 
				
			||||
  quill,
 | 
				
			||||
  placeholder
 | 
				
			||||
) {
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!quill || !quill.root) return;
 | 
				
			||||
    quill.root.dataset.placeholder = placeholder;
 | 
				
			||||
  }, [quill, placeholder]);
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useQuillPlaceholder;
 | 
				
			||||
@ -0,0 +1,31 @@
 | 
				
			||||
import { useEffect, useState } from 'react'
 | 
				
			||||
import deepEqual from './deepEqual.js'
 | 
				
			||||
 | 
				
			||||
function useQuillValueSync(quill, value) {
 | 
				
			||||
  const [selection, setSelection] = useState(null)
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (!quill) return
 | 
				
			||||
 | 
				
			||||
    const previous = quill.getContents()
 | 
				
			||||
    const current = value
 | 
				
			||||
 | 
				
			||||
    if (!deepEqual(previous, current)) {
 | 
				
			||||
      setSelection(quill.getSelection())
 | 
				
			||||
      if (typeof value === 'string') {
 | 
				
			||||
        quill.clipboard.dangerouslyPasteHTML(value, 'api')
 | 
				
			||||
      } else {
 | 
				
			||||
        quill.setContents(value)
 | 
				
			||||
      }
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, value, setSelection])
 | 
				
			||||
 | 
				
			||||
  useEffect(() => {
 | 
				
			||||
    if (quill && selection) {
 | 
				
			||||
      quill.setSelection(selection)
 | 
				
			||||
      setSelection(null)
 | 
				
			||||
    }
 | 
				
			||||
  }, [quill, selection, setSelection])
 | 
				
			||||
}
 | 
				
			||||
 | 
				
			||||
export default useQuillValueSync
 | 
				
			||||
@ -0,0 +1,8 @@
 | 
				
			||||
.task_comment_task{
 | 
				
			||||
  background: #fff;
 | 
				
			||||
  padding: 20px 30px 0;
 | 
				
			||||
  height: calc(100vh - 177px);
 | 
				
			||||
  box-sizing: border-box;
 | 
				
			||||
  overflow-y: auto;
 | 
				
			||||
  border-bottom: 1px solid rgba(244,244,244,1);
 | 
				
			||||
}
 | 
				
			||||
					Loading…
					
					
				
		Reference in new issue