diff --git a/public/react/src/AppConfig.js b/public/react/src/AppConfig.js
index 924bd810e..a180777bf 100644
--- a/public/react/src/AppConfig.js
+++ b/public/react/src/AppConfig.js
@@ -47,8 +47,8 @@ export function initAxiosInterceptors(props) {
// TODO 读取到package.json中的配置?
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参考来控制
diff --git a/public/react/src/forge/quillForEditor/FillBlot.js b/public/react/src/forge/quillForEditor/FillBlot.js
new file mode 100644
index 000000000..5e5e2aa77
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/FillBlot.js
@@ -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;
diff --git a/public/react/src/forge/quillForEditor/ImageBlot.js b/public/react/src/forge/quillForEditor/ImageBlot.js
new file mode 100644
index 000000000..5ff84b249
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/ImageBlot.js
@@ -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';
\ No newline at end of file
diff --git a/public/react/src/forge/quillForEditor/README.md b/public/react/src/forge/quillForEditor/README.md
new file mode 100644
index 000000000..c9c53b902
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/README.md
@@ -0,0 +1,95 @@
+
+## 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)
+ }
+ }
+
+
+
+
+ ````
+
diff --git a/public/react/src/forge/quillForEditor/deepEqual.js b/public/react/src/forge/quillForEditor/deepEqual.js
new file mode 100644
index 000000000..6f2b276bf
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/deepEqual.js
@@ -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;
diff --git a/public/react/src/forge/quillForEditor/index.js b/public/react/src/forge/quillForEditor/index.js
new file mode 100644
index 000000000..012743141
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/index.js
@@ -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 (
+
+ );
+}
+
+export default QuillForEditor;
diff --git a/public/react/src/forge/quillForEditor/index.scss b/public/react/src/forge/quillForEditor/index.scss
new file mode 100644
index 000000000..dd4eb0349
--- /dev/null
+++ b/public/react/src/forge/quillForEditor/index.scss
@@ -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;
+ }
+ }
+}
\ No newline at end of file