工单部分相关的都改成quill编辑器

This commit is contained in:
caicai8 2020-03-17 17:23:47 +08:00
parent 4875f2c944
commit 637ce0fc9a
13 changed files with 846 additions and 72 deletions

View File

@ -126,21 +126,10 @@ export function initAxiosInterceptors(props) {
// https://github.com/axios/axios/issues/1497
// TODO 读取到package.json中的配置
var proxy = "http://localhost:3000"
// proxy = "http://testbdweb.trustie.net"
// proxy = "http://testbdweb.educoder.net"
// proxy = "https://testeduplus2.educoder.net"
//proxy="http://47.96.87.25:48080"
// proxy="https://pre-newweb.educoder.net"
// proxy="https://test-newweb.educoder.net"
// proxy="https://test-jupyterweb.educoder.net"
// proxy="https://test-newweb.educoder.net"
// proxy="https://test-jupyterweb.educoder.net"
//proxy="http://192.168.2.63:3001"
var //proxy = "http://localhost:3000"
// proxy="http://123.59.135.93:56666"
proxy="http://localhost:3000"
proxy="http://123.59.135.93:56666"
// proxy="http://localhost:3000"
// 在这里使用requestMap控制避免用户通过双击等操作发出重复的请求
// 如果需要支持重复的请求考虑config里面自定义一个allowRepeat参考来控制
@ -236,7 +225,7 @@ export function initAxiosInterceptors(props) {
// let timestamp=railsgettimes(proxy);
// console.log(timestamp)
// `https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp`
railsgettimes( `${proxy}/api/main/first_stamp.json`);
// railsgettimes( `${proxy}/api/main/first_stamp.json`);
let newopens=md5(opens+timestamp)
config.url = `${proxy}${url}`;
if (config.url.indexOf('?') == -1) {
@ -248,7 +237,7 @@ export function initAxiosInterceptors(props) {
// 加api前缀
// railsgettimes(`http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp`);
railsgettimes( `/api/main/first_stamp.json`);
// railsgettimes( `/api/main/first_stamp.json`);
let newopens=md5(opens+timestamp)
config.url = url;
if (config.url.indexOf('?') == -1) {

View File

@ -106,31 +106,31 @@ export function getmyUrl(geturl) {
}
export function getUploadActionUrl(path, goTest) {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
return `${getUrl()}/api/attachments.json${isDev ? `?debug=${window._debugType || 'admin'}&randomcode=${newtimestamp}&client_key=${anewopens}` : `?randomcode=${newtimestamp}&client_key=${anewopens}`}`;
}
export function getUploadActionUrltwo(id) {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
return `${getUrlmys()}/api/shixuns/${id}/upload_data_sets.json${isDev ? `?debug=${window._debugType || 'admin'}&randomcode=${newtimestamp}&client_key=${anewopens}` : `?randomcode=${newtimestamp}&client_key=${anewopens}`}`
}
export function getUploadActionUrlthree() {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
return `${getUrlmys()}/api/jupyters/import_with_tpm.json${isDev ? `?debug=${window._debugType || 'admin'}&randomcode=${newtimestamp}&client_key=${anewopens}` : `?randomcode=${newtimestamp}&client_key=${anewopens}`}`
}
export function getUploadActionUrlOfAuth(id) {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
return `${getUrl()}/api/users/accounts/${id}/auth_attachment.json${isDev ? `?debug=${window._debugType || 'admin'}&randomcode=${newtimestamp}&client_key=${anewopens}` : `?randomcode=${newtimestamp}&client_key=${anewopens}`}`
}
export function getRandomNumber(type) {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
return type===true?`randomcode=${newtimestamp}&client_key=${anewopens}`:`?randomcode=${newtimestamp}&client_key=${anewopens}`
}
@ -149,7 +149,7 @@ export function getTaskUrlById(id) {
}
export function getRandomcode(url) {
Railsgettimes()
// Railsgettimes()
let anewopens=md5(newopens+newtimestamp);
if (url.indexOf('?') == -1) {

View File

@ -5,13 +5,20 @@ import axios from 'axios';
import Nav from './Nav';
import UploadComponent from '../Upload/Index';
import { getImageUrl } from 'educoder';
import {Modal, Col, Form, Input, Tooltip, Popconfirm, Pagination , Spin} from 'antd'
import NoneData from '../../modules/courses/coursesPublic/NoneData';
import Attachments from '../Upload/attachment'
import {Modal , Form, Input, Tooltip, Popconfirm, Pagination , Spin} from 'antd'
import Attachments from '../Upload/attachment';
import QuillForEditor from '../quillForEditor';
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html'
const TextArea = Input.TextArea;
const options = [
['bold', 'italic', 'underline'],
[{header: [1,2,3,false]}],
['blockquote', 'code-block'],
['link', 'image'],
['formula']
];
class Detail extends Component{
constructor(props){
@ -32,7 +39,9 @@ class Detail extends Component{
page:1,
search_count:undefined,
isSpin:false,
showFiles: true
showFiles: true,
quillValue:'',
quillFlag:false
}
}
@ -74,12 +83,26 @@ class Detail extends Component{
//添加评论
addjournals=()=>{
const { quillValue } = this.state;
if(!quillValue){
this.setState({
quillFlag:true
})
return;
}
let _html = '';
try {
_html = new QuillDeltaToHtmlConverter(quillValue.ops, {}).convert();
} catch (error) {
console.log(error);
}
this.props.form.validateFieldsAndScroll((err, values) => {
if(!err){
const { data, page, limit, fileList } = this.state;
const url = `/issues/${data.id}/journals.json`;
axios.post(url,{
...values,
content:_html,
issue_id:data.id,
attachment_ids:fileList
}).then(result=>{
@ -88,11 +111,12 @@ class Detail extends Component{
content: ""
});
this.setState({
showFiles: false
showFiles: false,
quillValue:'',
quillFlag:false
})
this.getjournalslist(page, limit);
this.props.showNotification("评论成功!");
// this.UploadFunc(undefined)
}
}).catch(error=>{
console.log(error);
@ -215,7 +239,7 @@ class Detail extends Component{
}
renderJournalList=(list)=>{
console.log(list);
// console.log(list);
if(list && list.length >0){
return(
list.map((item,key)=>{
@ -280,10 +304,18 @@ class Detail extends Component{
showFiles:flag
})
}
onContentChange=(value)=>{
if(value){
this.setState({
quillValue:value,
quillFlag:false
})
}
}
render(){
const { projectsId,orderId} = this.props.match.params;
const { data,journalsdata, page, limit, search_count, isSpin, isedit, showFiles } = this.state;
const { data,journalsdata, page, limit, search_count, isSpin, isedit, showFiles , quillValue , quillFlag } = this.state;
const { getFieldDecorator } = this.props.form;
const { current_user } = this.props;
const Paginations = (
@ -325,7 +357,10 @@ class Detail extends Component{
<div style={{display: (isedit && isedit === item.id) ? "none" : "block"}} >
{
item.content ?
<div>{item.content}</div>
<QuillForEditor
readOnly={true}
value={item.content}
/>
:
<div>
{this.renderJournalList(item.journal_details)}
@ -370,6 +405,8 @@ class Detail extends Component{
}
}
return(
<div className="main">
<div className="topWrapper">
@ -388,7 +425,7 @@ class Detail extends Component{
<span className="font-20" > { data && data.subject }</span>
</p>
<p className="mt10 color-grey-c">
<p className="mt15 color-grey-c">
<span className={data&&data.issue_status==="关闭"?"closedetail":"opendetail"}>{data&&data.issue_status==="关闭"?"关闭中":"开启中"} </span>
<span className="ml10 lineH32">
{ data && data.author_name} { data && data.created_at }创建{ data && data.journals_count && data.journals_count > 0 ?` · ${data.journals_count} 条评论`:""}
@ -411,7 +448,10 @@ class Detail extends Component{
<Link to={``}><img className="user_img" src={getImageUrl(`images/${data && data.author_picture}`)} alt=""/></Link>
<div className="detail_context">
<div className="detail_p">
{ data && data.description }
<QuillForEditor
readOnly={true}
value={ data && data.description}
/>
</div>
{
data && data.attachments && data.attachments.length > 0 ?
@ -435,15 +475,26 @@ class Detail extends Component{
<div className="df">
<Link to={``}><img className="user_img" src={getImageUrl(`images/${current_user && current_user.image_url}`)} alt=""/></Link>
<div className="new_context">
<Form.Item>
{getFieldDecorator('content', {
rules: [{
required: true, message: '请输入内容'
}],
})(
<TextArea placeholder="添加评论..." style={{height: "200px"}}/>
)}
</Form.Item>
<div className="quillContent">
<QuillForEditor
imgAttrs={{ width: '60px', height: '30px' }}
wrapStyle={{
height: '220px',
opacity:1,
}}
autoFocus={true}
style={{ height: '150px' }}
placeholder="添加评论..."
options={options}
value={quillValue}
onContentChange={this.onContentChange}
// showUploadImage={handleShowImage}
// onContentChange={handleContentChange}
/>
<p className="quillFlag">
{ quillFlag && <span className="">请输入评论内容</span>}
</p>
</div>
<UploadComponent load={this.UploadFunc} isComplete={showFiles} changeIsComplete={this.changeIsComplete}></UploadComponent>
<p className="clearfix mt15">
<a className="topWrapper_btn fr" type="submit" onClick={this.addjournals}>评论</a>
@ -458,7 +509,7 @@ class Detail extends Component{
</div>
</div>
</div>
<div className="list-left DetailRight mt10">
<div className="list-left DetailRight mt10" style={{backgroundColor:"#fff"}}>
<p>
<span className="span_title">分支</span>
<span>{

View File

@ -1,17 +1,25 @@
import React , { Component } from "react";
import { Form , Input , Select } from 'antd';
import {Link} from 'react-router-dom';
import { getImageUrl } from 'educoder';
import Nav from './Nav';
import UploadComponent from '../Upload/Index';
import QuillForEditor from '../quillForEditor';
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html'
import './order.css';
import axios from 'axios';
const Option = Select.Option;
const TextArea = Input.TextArea;
const options = [
['bold', 'italic', 'underline'],
[{header: [1,2,3,false]}],
['blockquote', 'code-block'],
['link', 'image'],
['formula']
];
class New extends Component{
constructor(props){
super(props);
@ -27,7 +35,8 @@ class New extends Component{
done_ratio:"0%",
issue_chosen:undefined,
branches:undefined,
fileList:undefined
fileList:undefined,
description:undefined
}
}
@ -94,9 +103,16 @@ class New extends Component{
if(values.issue_tag_ids.length > 0){
values.issue_tag_ids = [values.issue_tag_ids]
}
const { description } = this.state;
let _html = '';
try {
_html = new QuillDeltaToHtmlConverter(description.ops, {}).convert();
} catch (error) {
console.log(error);
}
axios.post(url,{
...values,
description:_html,
attachment_ids:fileList
}).then(result=>{
if(result){
@ -118,10 +134,16 @@ class New extends Component{
})
}
onContentChange=(value)=>{
this.setState({
description:value
})
}
render(){
const { getFieldDecorator } = this.props.form;
const { current_user } = this.props;
const { issue_tag_ids , fixed_version_id , branch_name , status_id , tracker_id , issue_type ,assigned_to_id , priority_id , done_ratio,
const { issue_tag_ids , fixed_version_id , branch_name , status_id , tracker_id , description ,assigned_to_id , priority_id , done_ratio,
issue_chosen , branches } = this.state;
return(
@ -132,7 +154,7 @@ class New extends Component{
<Form>
<div className="f-wrap-between mt20" style={{alignItems:"flex-start"}}>
<div className="list-right df">
<Link to={``}><img class="user_img" src={getImageUrl(`images/${current_user && current_user.image_url}`)} alt=""/></Link>
<img class="user_img" src={getImageUrl(`images/${current_user && current_user.image_url}`)} alt=""/>
<div className="new_context">
<Form.Item>
{getFieldDecorator('subject', {
@ -143,20 +165,35 @@ class New extends Component{
<Input placeholder="标题"/>
)}
</Form.Item>
<Form.Item>
<div className="quillContent" style={{marginBottom:"20px"}}>
<QuillForEditor
value = {description}
imgAttrs={{ width: '60px', height: '30px' }}
wrapStyle={{
height: '220px',
opacity:1,
}}
// autoFocus={true}
style={{ height: '170px' }}
placeholder="请输入工单的描述..."
options={options}
onContentChange={this.onContentChange}
/>
</div>
{/* <Form.Item>
{getFieldDecorator('description', {
rules: [],
})(
<TextArea placeholder="请输入工单的描述..." style={{height:"300px"}}/>
)}
</Form.Item>
</Form.Item> */}
<UploadComponent load={this.UploadFunc} showNotification={this.props.showNotification} isComplete={true}></UploadComponent>
<p className="clearfix mt15">
<a className="topWrapper_btn fr" type="submit" onClick={this.handleSubmit}>创建Issue</a>
</p>
</div>
</div>
<div className="list-left" style={{paddingRight:"0px",paddingLeft:"15px",paddingTop:"10px"}}>
<div className="list-left" style={{paddingRight:"0px",paddingLeft:"0px",width:"25%",backgroundColor:"#fff"}}>
<div className="list-l-panel">
<Form.Item>
{getFieldDecorator('branch_name', {

View File

@ -5,15 +5,22 @@ import axios from 'axios';
import Nav from './Nav';
import UploadComponent from '../Upload/Index';
import { getImageUrl } from 'educoder';
import{ Modal,Col,Form,Input,Tooltip,Select } from 'antd'
import NoneData from '../../modules/courses/coursesPublic/NoneData';
import{ Modal , Form , Input , Select } from 'antd'
import Attachments from '../Upload/attachment'
import Attachments from '../Upload/attachment';
import QuillForEditor from '../quillForEditor';
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html'
const TextArea = Input.TextArea;
const Option = Select.Option;
const options = [
['bold', 'italic', 'underline'],
[{header: [1,2,3,false]}],
['blockquote', 'code-block'],
['link', 'image'],
['formula']
];
class UpdateDetail extends Component{
constructor(props){
super(props);
@ -100,6 +107,12 @@ class UpdateDetail extends Component{
});
};
onContentChange=(value)=>{
this.setState({
textcount:value
})
}
changmodelname=(e)=>{
this.setState({
@ -145,11 +158,18 @@ class UpdateDetail extends Component{
if(values.assigned_to_id===0){
values.assigned_to_id = ""
}
const { textcount } = this.state;
let _html = '';
try {
_html = new QuillDeltaToHtmlConverter(textcount.ops, {}).convert();
} catch (error) {
console.log(error);
}
axios.put(url,{
project_id:projectsId,
subject:subject,
id: orderId,
description:this.state.textcount,
description:_html,
attachment_ids:fileList,
...values
}).then(result=>{
@ -180,7 +200,7 @@ class UpdateDetail extends Component{
<Form>
<div className="f-wrap-between mt20" style={{alignItems:"flex-start"}}>
<div className="list-right df" >
<Link to={``}><img class="user_img" src={getImageUrl(`images/${current_user && current_user.image_url}`)} alt=""/></Link>
<img class="user_img" src={getImageUrl(`images/${current_user && current_user.image_url}`)} alt=""/>
<div className="new_context">
<Form.Item>
{getFieldDecorator('subject', {
@ -192,14 +212,21 @@ class UpdateDetail extends Component{
<Input placeholder="标题" onChange={this.changmodelname}/>
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('description', {
rules: [],
initialValue: textcount
})(
<TextArea placeholder="请输入工单的描述..." style={{height:"300px"}} onChange={this.changmodelcount}/>
)}
</Form.Item>
<div className="quillContent" style={{marginBottom:"20px"}}>
<QuillForEditor
value = {textcount}
imgAttrs={{ width: '60px', height: '30px' }}
wrapStyle={{
height: '220px',
opacity:1,
}}
// autoFocus={true}
style={{ height: '170px' }}
placeholder="请输入工单的描述..."
options={options}
onContentChange={this.onContentChange}
/>
</div>
<UploadComponent load={this.UploadFunc} showNotification={this.props.showNotification} isComplete={true}></UploadComponent>
{
get_attachments ?
@ -213,7 +240,7 @@ class UpdateDetail extends Component{
</p>
</div>
</div>
<div className="list-left" style={{paddingRight:"0px",paddingLeft:"15px",paddingTop:"10px"}}>
<div className="list-left" style={{paddingRight:"0px",paddingLeft:"0px",width:"25%",backgroundColor:"#fff"}}>
<div className="list-l-panel">
<Form.Item
label="分支"

View File

@ -6,6 +6,18 @@
border-bottom: 1px solid #EEEEEE;
flex-wrap: wrap;
}
.quillContent{
position: relative;
margin-bottom: 4px;
}
.quillFlag{
position: absolute;
bottom:0px;
left:6px;
height: 20px;
line-height: 18px;
color: red;
}
.topmilepost{
padding: 20px 0;
@ -354,9 +366,6 @@
width: 80%;
padding-left: 80px;
}
.detail_ptitlecount{
padding: 15px;
}
.tagList > div:last-child{
border-bottom: none;

View File

@ -1,4 +1,6 @@
.newMain{
background-color: #fff;
}
.main{
width: 1200px;
margin:20px auto;

View File

@ -0,0 +1,35 @@
/*
* @Description: 填空
* @Author: tangjiang
* @Github:
* @Date: 2020-01-06 09:02:29
* @LastEditors : tangjiang
* @LastEditTime : 2020-02-05 10:44:01
*/
import Quill from 'quill';
let Inline = Quill.import('blots/inline');
// const BlockEmbed = Quill.import('blots/embed');
class FillBlot extends Inline {
static create (value) {
const node = super.cerate(value);
// node.classList.add('icon icon-bianji2');
// node.setAttribute('data-fill', 'fill');
console.log('编辑器值===》》》》》', value);
node.setAttribute('data_index', value.data_index);
node.nodeValue = value.text;
return node;
}
static value (node) {
return {
// dataSet: node.getAttribute('data-fill'),
data_index: node.getAttribute('data_index')
}
}
}
FillBlot.blotName = "fill";
FillBlot.tagName = "span";
export default FillBlot;

View File

@ -0,0 +1,70 @@
/*
* @Description: 重写图片
* @Author: tangjiang
* @Github:
* @Date: 2019-12-16 15:50:45
* @LastEditors : tangjiang
* @LastEditTime : 2019-12-31 13:59:02
*/
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);
// console.log('~~~~~~~~~~~', node, value);
node.addEventListener('click', function () {
value.onclick(value.url);
}, false);
if (value.width) {
node.setAttribute('width', value.width);
}
if (value.height) {
node.setAttribute('height', value.height);
}
if (value.id) {
node.setAttribute('id', value.id);
}
// 宽度和高度都不存在时,
if (!value.width && !value.height) {
// node.setAttribute('display', 'block');
node.setAttribute('width', '100%');
}
// node.setAttribute('style', { cursor: 'pointer' });
// if (node.onclick) {
// console.log('image 有图片点击事件======》》》》》》');
// // node.setAttribute('onclick', node.onCLick);
// }
// 给图片添加点击事件
// 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'),
id: node.id,
// style: node.style
};
}
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';

View File

@ -0,0 +1,95 @@
<!--
* @Description:
* @Author: tangjiang
* @Github:
* @Date: 2020-01-06 16:20:03
* @LastEditors : tangjiang
* @LastEditTime : 2020-01-09 09:45:29
-->
## QuillForEditor 使用 [https://quilljs.com/]
### 导入
- 目录 src/common/quillForEditor (默认加载当前文件夹下的 index.js 文件)
### 参数
| 字段 | 描述 |
| ----- | ----- |
| placeholder | 提示信息 |
| readOnly | 只读(只读取模式时,没有 工具栏且内容不可编辑通常用于展示quill内容) |
| autoFocus | 自动获得焦点 |
| options | 配置参数, 指定工具栏内容 |
| value | 文本编辑器内容 |
| imgAttrs | 指定上传图片的尺寸 { width: 'xxpx}, height: 'xxpx'|
| style | 指定quill容器样式 |
| wrapStyle | 指定包裹quill容器的样式|
| onContentChange | 当编辑器内容变化时调用此回调函数(注: 此时返回的内容为对象,提交到后台时需要格式成 JSON 字符串: JSON.stringify(xx)) |
| showUploadImage | 点击放大上传成功后的图片, 返回上传成功后的图片 url, (评论时点击图片这么大)|
### 添加工具栏
- 默认所有的
```
const options = [
'bold', // 加粗
'italic', // 斜体
'underline', // 下划线
{size: ['12px', '14px', '16px', '18px', '20px']}, // 字体大小
{align: []}, // 对齐方式
{list: 'ordered'}, // 有序列表
{list: 'bullet'}, // 无序列表
{script: 'sub'}, // 下标 x2
{script: 'super'}, // 上标 平方 (x2)
{ 'color': [] }, // 字体颜色
{ 'background': [] }, // 背景色
{header: [1,2,3,4,5,false]}, // H1,H2 ...
'blockquote', // 文件左边加一个边框样式
'code-block', // 块内容
'link', // 链接
'image', // 图片
'video', // 视频
'formula', // 数学公式
'clean' // 清除
]
```
### 使用
````
编辑模式是放不大图片的
import QuillForEditor from 'xxx';
// 指定需要显示的工具栏信息, 不指定加载全部
const options = [
];
/**
* @description 获取编辑器返回的内容
* @params [Object] value 编辑器内容
*/
const handleCtxChange = (value) => {
// 编辑器内容非空判断
const _text = quill.getText();
const reg = /^[\s\S]*.*[^\s][\s\S]*$/;
if (!reg.test(_text)) {
// 处理编辑器内容为空
} else {
// 提交到后台的内容需要处理一下;
value = JSON.stringify(value)
}
}
<QuillForEditor
options={options}
onContentChange={handleCtxChange}
>
</QuillForEditor>
````

View File

@ -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;

View File

@ -0,0 +1,280 @@
/*
* @Description: quill 编辑器
* @Author: tangjiang
* @Github:
* @Date: 2019-12-18 08:49:30
* @LastEditors : tangjiang
* @LastEditTime : 2020-02-05 11:23:03
*/
import './index.scss';
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';
import FillBlot from './FillBlot';
const Size = Quill.import('attributors/style/size');
const Font = Quill.import('formats/font');
// const Color = Quill.import('attributes/style/color');
Size.whitelist = ['12px', '14px', '16px', '18px', '20px', false];
Font.whitelist = ['SimSun', 'SimHei','Microsoft-YaHei','KaiTi','FangSong','Arial','Times-New-Roman','sans-serif'];
window.Quill = Quill;
window.katex = katex;
Quill.register(ImageBlot);
Quill.register(Size);
Quill.register(Font, true);
// Quill.register({'modules/toolbar': Toolbar});
Quill.register({
'formats/fill': FillBlot
});
// Quill.register(Color);
function QuillForEditor ({
placeholder,
readOnly,
autoFocus = false,
options,
value,
imgAttrs = {}, // 指定图片的宽高
style = {},
wrapStyle = {},
showUploadImage,
onContentChange,
addFill, // 点击填空成功的回调
deleteFill // 删除填空,返回删除的下标
// getQuillContent
}) {
// toolbar 默认值
const defaultConfig = [
'bold', 'italic', 'underline',
{size: ['12px', '14px', '16px', '18px', '20px']},
{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 [fillCount, setFillCount] = useState(0);
const [quillCtx, setQuillCtx] = useState({});
// 文本内容变化时
const handleOnChange = content => {
// getQuillContent && getQuillContent(quill);
onContentChange && onContentChange(content, quill);
};
const renderOptions = options || defaultConfig;
const bindings = {
tab: {
key: 9,
handler: function () {
console.log('调用了tab=====>>>>');
}
},
backspace: {
key: 'Backspace',
/**
* @param {*} range
* { index, // 删除元素的位置
* length // 删除元素的个数, 当删除一个时, length=0 其它等于删除的元素的个数
* }
* @param {*} context 上下文
*/
handler: function (range, context) {
/**
* index: 删除元素的位置
* length: 删除元素的个数
*/
const {index, length} = range;
const _start = length === 0 ? index - 1 : index;
const _length = length || 1;
let delCtx = this.quill.getText(_start, _length); // 删除的元素
// aa
const reg = /▁/g;
const delArrs = delCtx.match(reg);
if (delArrs) {
const r = window.confirm('确定要删除吗?');
if (r) {
let leaveCtx; // 获取删除元素之前的内容
if (length === 0) {
leaveCtx = this.quill.getText(0, index - 1);
} else {
leaveCtx = this.quill.getText(0, index);
}
const leaveArrs = leaveCtx.match(reg);
const leaveLen = (leaveArrs || []).length;
let delIndexs = [];
// 获取删除元素的下标
delArrs.forEach((item, i) => {
leaveLen === 0 ? delIndexs.push(i) : delIndexs.push(leaveLen + i);
});
deleteFill && deleteFill(delIndexs); // 调用删除回调, 返回删除的元素下标[]
return true
} else {
return false;
}
}
return true;
}
}
};
// quill 配置信息
const quillOption = {
modules: {
toolbar: renderOptions,
keyboard: {
bindings: bindings
}
// toolbar: {
// container: renderOptions
// }
},
readOnly,
placeholder,
theme: readOnly ? 'bubble' : 'snow',
};
useEffect(() => {
const quillNode = document.createElement('div');
editorRef.current.appendChild(quillNode);
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
});
}
}
});
// 处理填空
_quill.getModule('toolbar').addHandler('fill', (e) => {
// alert(1111);
setFillCount(fillCount + 1);
const range = _quill.getSelection(true);
_quill.insertText(range.index, '▁');
addFill && addFill(); // 调用添加回调
});
}, []);
// 设置值
useEffect(() => {
if (!quill) return
const previous = quill.getContents()
if (value && value.hasOwnProperty('ops')) {
// console.log(value.ops);
const ops = value.ops || [];
ops.forEach((item, i) => {
if (item.insert['image']) {
item.insert['image'] = Object.assign({}, item.insert['image'], {style: { cursor: 'pointer' }, onclick: (url) => showUploadImage(url)});
}
});
}
const current = value
if (!deepEqual(previous, current)) {
setSelection(quill.getSelection())
if (typeof value === 'string' && value) {
// debugger
quill.clipboard.dangerouslyPasteHTML(value, 'api');
if (autoFocus) {
quill.focus();
} else {
quill.blur();
}
} else {
quill.setContents(value)
if (autoFocus) quill.focus();
}
}
}, [quill, value, setQuill, autoFocus]);
// 清除选择区域
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 = (delta, oldDelta, source) => {
const _ctx = quill.getContents();
setQuillCtx(_ctx);
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;

View File

@ -0,0 +1,132 @@
.quill_editor_for_react_area{
// background: #fff;
// margin: 0 15px;
.ql-editing{
left: 0 !important;
}
.ql-editor{
img{
cursor: pointer;
}
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
content: '12px';
font-size: 12px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
content: '14px';
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
content: '16px';
font-size: 16px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
content: '18px';
font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
content: '20px';
font-size: 20px;
}
//默认的样式
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '14px';
font-size: 14px;
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimSun]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimSun]::before {
content: "宋体";
font-family: "SimSun";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=SimHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=SimHei]::before {
content: "黑体";
font-family: "SimHei";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Microsoft-YaHei]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Microsoft-YaHei]::before {
content: "微软雅黑";
font-family: "Microsoft YaHei";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=KaiTi]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=KaiTi]::before {
content: "楷体";
font-family: "KaiTi";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=FangSong]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=FangSong]::before {
content: "仿宋";
font-family: "FangSong";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Arial]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Arial]::before {
content: "Arial";
font-family: "Arial";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=Times-New-Roman]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=Times-New-Roman]::before {
content: "Times New Roman";
font-family: "Times New Roman";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=sans-serif]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=sans-serif]::before {
content: "sans-serif";
font-family: "sans-serif";
}
.ql-font-SimSun {
font-family: "SimSun";
}
.ql-font-SimHei {
font-family: "SimHei";
}
.ql-font-Microsoft-YaHei {
font-family: "Microsoft YaHei";
}
.ql-font-KaiTi {
font-family: "KaiTi";
}
.ql-font-FangSong {
font-family: "FangSong";
}
.ql-font-Arial {
font-family: "Arial";
}
.ql-font-Times-New-Roman {
font-family: "Times New Roman";
}
.ql-font-sans-serif {
font-family: "sans-serif";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "微软雅黑";
font-family: "Microsoft YaHei";
}
// 填空图标
.ql-snow .ql-fill{
display: inline-block;
position: relative;
color: #05101A;
// font-size: 18px;
vertical-align: top;
&::before{
position: absolute;
left: 50%;
top: -1px;
content: '\e709';
font-family: 'iconfont';
margin-left: -7px;
}
}
}