diff --git a/public/react/package.json b/public/react/package.json
index 66db5e6f8..144292feb 100644
--- a/public/react/package.json
+++ b/public/react/package.json
@@ -61,6 +61,7 @@
"prop-types": "^15.6.1",
"qs": "^6.6.0",
"quill": "^1.3.7",
+ "quill-delta-to-html": "^0.11.0",
"raf": "3.4.0",
"rc-form": "^2.1.7",
"rc-pagination": "^1.16.2",
diff --git a/public/react/src/common/components/comment/CommentForm.js b/public/react/src/common/components/comment/CommentForm.js
new file mode 100644
index 000000000..73e36cff9
--- /dev/null
+++ b/public/react/src/common/components/comment/CommentForm.js
@@ -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 (
+
+ );
+}
+
+export default Form.create()(CommentForm);
diff --git a/public/react/src/common/components/comment/CommentIcon.js b/public/react/src/common/components/comment/CommentIcon.js
new file mode 100644
index 000000000..5440e2579
--- /dev/null
+++ b/public/react/src/common/components/comment/CommentIcon.js
@@ -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 (
+
+
+ { count }
+
+ )
+}
+
+export default CommentIcon;
diff --git a/public/react/src/common/components/comment/CommentItem.js b/public/react/src/common/components/comment/CommentItem.js
new file mode 100644
index 000000000..19da645f5
--- /dev/null
+++ b/public/react/src/common/components/comment/CommentItem.js
@@ -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: (确定要删除该条回复吗?
),
+ onOk () {
+ console.log('点击了删除');
+ }
+ });
+ // ModalConfirm('提示', (确定要删除该条回复吗?
), () => {
+ // console.log('点击了删除');
+ // });
+ }
+
+ // 评论头像
+ const commentAvatar = (url) => (
+
+ );
+
+ // 评论信息
+ const commentInfo = () => (
+
+ 用户名
+ {moment(new Date(), 'YYYYMMDD HHmmss').fromNow()}
+
+
+ );
+
+ // 评论内容
+ const commentCtx = (ctx) => (
+
+ 这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容
+
+ );
+
+ // 加载更多
+ const handleOnLoadMore = () => {
+ if (!arrow) {
+ // 展开所有
+ } else {
+ // 收起
+ }
+ setArrow(!arrow);
+ };
+
+ // 评论追加内容
+ const commentAppend = () => {
+
+ return (
+
+ -
+ {commentAvatar()}
+
+ {commentInfo()}
+ {commentCtx()}
+
+
+ -
+ {commentAvatar()}
+
+ {commentInfo()}
+ {commentCtx()}
+
+
+ -
+ {commentAvatar()}
+
+ {commentInfo()}
+ {commentCtx()}
+
+
+
+ -
+
展开其余23条评论
+
+
+
+
+
+ );
+ };
+ // 点击图标
+ const handleIconClick = () => {}
+
+ // 点击评论icon
+ const handleClickMessage = () => {
+ setShowQuill(true);
+ }
+
+ // 点击取消
+ const handleClickCancel = () => {
+ setShowQuill(false);
+ }
+
+ // 点击保存
+ const handleClickSubmit = (content) => {
+ // 保存并关闭
+ setShowQuill(false);
+ console.log('获取保存内容', content);
+ }
+
+ return (
+
+ {commentAvatar()}
+
+ {commentInfo()}
+ {commentCtx()}
+
+ {commentAppend()}
+
+
+
+ {/* 回复 */}
+
+ {/* 点赞 */}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default CNotificationHOC() (CommentItem);
diff --git a/public/react/src/common/components/comment/CommentList.js b/public/react/src/common/components/comment/CommentList.js
new file mode 100644
index 000000000..9d8cde87b
--- /dev/null
+++ b/public/react/src/common/components/comment/CommentList.js
@@ -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 (
+
+ );
+}
+
+export default CommentList;
diff --git a/public/react/src/common/components/comment/index.js b/public/react/src/common/components/comment/index.js
new file mode 100644
index 000000000..f0ecf3309
--- /dev/null
+++ b/public/react/src/common/components/comment/index.js
@@ -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 (
+
+
+
+
+ );
+}
+
+export default Comment;
diff --git a/public/react/src/common/components/comment/index.scss b/public/react/src/common/components/comment/index.scss
new file mode 100644
index 000000000..816e6da6c
--- /dev/null
+++ b/public/react/src/common/components/comment/index.scss
@@ -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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/react/src/common/quillForEditor/ImageBlot.js b/public/react/src/common/quillForEditor/ImageBlot.js
new file mode 100644
index 000000000..091bd2c1f
--- /dev/null
+++ b/public/react/src/common/quillForEditor/ImageBlot.js
@@ -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';
\ No newline at end of file
diff --git a/public/react/src/common/quillForEditor/deepEqual.js b/public/react/src/common/quillForEditor/deepEqual.js
new file mode 100644
index 000000000..6f2b276bf
--- /dev/null
+++ b/public/react/src/common/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/common/quillForEditor/index.js b/public/react/src/common/quillForEditor/index.js
new file mode 100644
index 000000000..6e6c01886
--- /dev/null
+++ b/public/react/src/common/quillForEditor/index.js
@@ -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 (
+
+ );
+}
+
+export default QuillForEditor;
diff --git a/public/react/src/common/reactQuill/ImageBlot.js b/public/react/src/common/reactQuill/ImageBlot.js
new file mode 100644
index 000000000..091bd2c1f
--- /dev/null
+++ b/public/react/src/common/reactQuill/ImageBlot.js
@@ -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';
\ No newline at end of file
diff --git a/public/react/src/common/reactQuill/ReactQuill.js b/public/react/src/common/reactQuill/ReactQuill.js
new file mode 100644
index 000000000..1b4209409
--- /dev/null
+++ b/public/react/src/common/reactQuill/ReactQuill.js
@@ -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 (
+
+ );
+}
+
+export default ReactQuill;
diff --git a/public/react/src/common/reactQuill/deepEqual.js b/public/react/src/common/reactQuill/deepEqual.js
new file mode 100644
index 000000000..6f2b276bf
--- /dev/null
+++ b/public/react/src/common/reactQuill/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/common/reactQuill/flatten.js b/public/react/src/common/reactQuill/flatten.js
new file mode 100644
index 000000000..237cb543f
--- /dev/null
+++ b/public/react/src/common/reactQuill/flatten.js
@@ -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;
diff --git a/public/react/src/common/reactQuill/index.js b/public/react/src/common/reactQuill/index.js
new file mode 100644
index 000000000..56a1a8d1f
--- /dev/null
+++ b/public/react/src/common/reactQuill/index.js
@@ -0,0 +1,108 @@
+/*
+ * @Description: 入口文件
+ * @Author: tangjiang
+ * @Github:
+ * @Date: 2019-12-17 10:41:48
+ * @LastEditors: tangjiang
+ * @LastEditTime: 2019-12-17 20:34:40
+ */
+import React, { useState, useCallback, useEffect } from 'react';
+import ReactQuill from './lib';
+
+function Wrapper (props) {
+ // 默认工具栏配置项
+ const toolbarConfig = [
+ ['bold', 'italic', 'underline'],
+ [{align: []}, {list: 'ordered'}, {list: 'bullet'}], // 列表
+ [{script: 'sub'}, {script: 'super'}],
+ [{header: [1,2,3,4,5,false]}],
+ ['blockquote', 'code-block'],
+ ['link', 'image', 'video'],
+ ['formula'],
+ ['clean']
+ ];
+
+ const [placeholder] = useState(props.placeholder || 'placeholder');
+ const [disableBold] = useState(false);
+ const [value, setValue] = useState(props.value || '');
+ const [toolbar, setToolbar] = useState(toolbarConfig);
+ const [theme, setTheme] = useState(props.theme || 'snow');
+ const [readOnly] = useState(props.readOnly || false);
+
+ const {
+ onContentChagne, // 当编辑器内容变化时调用该函数
+ showUploadImage, // 显示上传图片, 返回url,主要用于点击图片放大
+ } = props;
+
+ // 配置信息
+ const options = {
+ modules: {
+ toolbar: toolbar,
+ clipboard: {
+ matchVisual: false
+ }
+ },
+ readOnly: readOnly,
+ theme: theme
+ }
+ // 配置信息
+ useEffect (() => {
+ if (props.options) {
+ setToolbar(props.options);
+ }
+ setTheme(props.theme || 'snow');
+ setValue(props.value);
+ }, [props]);
+
+ // 当内容变化时
+ const handleOnChange = useCallback(
+ contents => {
+ if (disableBold) {
+ setValue({
+ ops: contents.ops.map(x => {
+ x = {...x};
+ if (x && x.attributes && x.attributes.bold) {
+ x.attributes = { ...x.attributes };
+ delete x.attributes.bold;
+ if (!Object.keys(x.attributes).length) {
+ delete x.attributes;
+ }
+ }
+ return x;
+ })
+ });
+ } else {
+ setValue(contents);
+ }
+ onContentChagne && onContentChagne(contents);
+ }, [disableBold]
+ );
+
+ // 图片上传
+ const handleUploadImage = (files) => {
+ console.log('选择的图片信息', files);
+ }
+
+ // 显示图片
+ const handleShowUploadImage = (url) => {
+ // console.log('上传的图片url:', url);
+ showUploadImage && showUploadImage(url);
+ }
+
+ return (
+
+ handleShowUploadImage(url)}
+ />
+
+ );
+}
+
+export default Wrapper;
+// ReactDOM.render(
, document.querySelector('#root'));
diff --git a/public/react/src/common/reactQuill/index.scss b/public/react/src/common/reactQuill/index.scss
new file mode 100644
index 000000000..b6da52bf5
--- /dev/null
+++ b/public/react/src/common/reactQuill/index.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/public/react/src/common/reactQuill/lib.js b/public/react/src/common/reactQuill/lib.js
new file mode 100644
index 000000000..430a95bb7
--- /dev/null
+++ b/public/react/src/common/reactQuill/lib.js
@@ -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 };
diff --git a/public/react/src/common/reactQuill/useDeepEqualMemo.js b/public/react/src/common/reactQuill/useDeepEqualMemo.js
new file mode 100644
index 000000000..948e21781
--- /dev/null
+++ b/public/react/src/common/reactQuill/useDeepEqualMemo.js
@@ -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;
diff --git a/public/react/src/common/reactQuill/useMountQuill.js b/public/react/src/common/reactQuill/useMountQuill.js
new file mode 100644
index 000000000..c2313c480
--- /dev/null
+++ b/public/react/src/common/reactQuill/useMountQuill.js
@@ -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 = `
+
+ Loading Katex...
+
+ `
+ }
+ // 清空内容
+ 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;
diff --git a/public/react/src/common/reactQuill/useQuill.js b/public/react/src/common/reactQuill/useQuill.js
new file mode 100644
index 000000000..b959dbc52
--- /dev/null
+++ b/public/react/src/common/reactQuill/useQuill.js
@@ -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;
diff --git a/public/react/src/common/reactQuill/useQuillOnChange.js b/public/react/src/common/reactQuill/useQuillOnChange.js
new file mode 100644
index 000000000..45333a4e1
--- /dev/null
+++ b/public/react/src/common/reactQuill/useQuillOnChange.js
@@ -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;
diff --git a/public/react/src/common/reactQuill/useQuillPlaceholder.js b/public/react/src/common/reactQuill/useQuillPlaceholder.js
new file mode 100644
index 000000000..ccc341568
--- /dev/null
+++ b/public/react/src/common/reactQuill/useQuillPlaceholder.js
@@ -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;
diff --git a/public/react/src/common/reactQuill/useQuillValueSync.js b/public/react/src/common/reactQuill/useQuillValueSync.js
new file mode 100644
index 000000000..696d88949
--- /dev/null
+++ b/public/react/src/common/reactQuill/useQuillValueSync.js
@@ -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
diff --git a/public/react/src/modules/developer/DeveloperHome.js b/public/react/src/modules/developer/DeveloperHome.js
index 5f787659e..73a5c37a1 100644
--- a/public/react/src/modules/developer/DeveloperHome.js
+++ b/public/react/src/modules/developer/DeveloperHome.js
@@ -16,7 +16,7 @@ import MultipTags from './components/multiptags';
// import { Link } from 'react-router-dom';
import CONST from '../../constants';
import { withRouter } from 'react-router';
-import { toStore } from 'educoder';
+import { toStore, CNotificationHOC } from 'educoder';
// import MyIcon from '../../common/components/MyIcon';
const {tagBackground, diffText} = CONST;
@@ -249,17 +249,26 @@ class DeveloperHome extends React.PureComponent {
// 删除
handleClickDelete = (record) => {
const { deleteItem } = this.props;
- Modal.confirm({
- title: '删除',
+ this.props.confirm({
+ title: '提示',
content: `确定要删除${record.name}吗?`,
- okText: '确定',
- cancelText: '取消',
onOk () {
// 调用删除接口
console.log(record.identifier);
deleteItem(record.identifier);
}
});
+ // Modal.confirm({
+ // title: '删除',
+ // content: `确定要删除${record.name}吗?`,
+ // okText: '确定',
+ // cancelText: '取消',
+ // onOk () {
+ // // 调用删除接口
+ // console.log(record.identifier);
+ // deleteItem(record.identifier);
+ // }
+ // });
}
// table条件变化时
handleTableChange = (pagination, filters, sorter) => {
@@ -562,5 +571,5 @@ const mapDispatchToProps = (dispatch) => ({
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
-)(DeveloperHome));
+)(CNotificationHOC() (DeveloperHome)));
// export default DeveloperHome;
diff --git a/public/react/src/modules/developer/components/controlSetting/index.js b/public/react/src/modules/developer/components/controlSetting/index.js
index 587e1bee9..b5c9222ef 100644
--- a/public/react/src/modules/developer/components/controlSetting/index.js
+++ b/public/react/src/modules/developer/components/controlSetting/index.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-27 16:02:36
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 17:32:33
+ * @LastEditTime: 2019-12-19 10:47:44
*/
import './index.scss';
import React, { useState, useRef } from 'react';
@@ -30,7 +30,7 @@ const ControlSetting = (props) => {
// debuggerCode,
// startDebuggerCode, // 外部存入
onDebuggerCode,
- updateCode,
+ // updateCode,
onSubmitForm
} = props;
const [defaultActiveKey, setDefaultActiveKey] = useState('1'); // 当前选中的tab
@@ -84,7 +84,7 @@ const ControlSetting = (props) => {
diff --git a/public/react/src/modules/developer/components/controlSetting/index.scss b/public/react/src/modules/developer/components/controlSetting/index.scss
index 97838ce5c..31beda8a5 100644
--- a/public/react/src/modules/developer/components/controlSetting/index.scss
+++ b/public/react/src/modules/developer/components/controlSetting/index.scss
@@ -2,7 +2,8 @@
position: absolute;
bottom: 0;
width: 100%;
- background:rgba(30,30,30,1);
+ // background: red;
+ // background:rgba(30,30,30,1);
// height: 56px;
.control_tab{
position: absolute;
@@ -52,8 +53,8 @@
height: 56px;
padding-right: 30px;
padding-left: 10px;
- // background: #000;
- background:rgba(48,48,48,1);
+ background: rgba(18,28,36,1);
+ // background:rgba(48,48,48,1);
}
.setting_drawer{
diff --git a/public/react/src/modules/developer/components/execResult/index.js b/public/react/src/modules/developer/components/execResult/index.js
index 32bbbee91..6f9341b9a 100644
--- a/public/react/src/modules/developer/components/execResult/index.js
+++ b/public/react/src/modules/developer/components/execResult/index.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-28 08:44:54
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-10 09:24:02
+ * @LastEditTime: 2019-12-19 10:44:16
*/
import './index.scss';
import React, { useState, useEffect } from 'react';
@@ -38,6 +38,15 @@ function ExecResult (props) {
);
+
+ const renderError = () => (
+
+
+ 未知异常
+
+
+ )
+
const renderFinish = () => {
const {
error_line,
@@ -60,6 +69,7 @@ function ExecResult (props) {
)
}
+ // console.log('执行结果====》》》》', status);
const excuteCtx = (state) => {
if (state === 0) {
return (
@@ -118,6 +128,8 @@ function ExecResult (props) {
setRenderCtx(() => (readerLoaded));
} else if ('finish' === excuteState) {
setRenderCtx(() => (renderFinish));
+ } else if ('error' === excuteState) {
+ setRenderCtx(() => (renderError))
}
}, [excuteState]);
diff --git a/public/react/src/modules/developer/components/initTabCtx/index.js b/public/react/src/modules/developer/components/initTabCtx/index.js
index 3834a3e11..3e707daa1 100644
--- a/public/react/src/modules/developer/components/initTabCtx/index.js
+++ b/public/react/src/modules/developer/components/initTabCtx/index.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-27 19:46:14
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 17:38:42
+ * @LastEditTime: 2019-12-19 10:47:05
*/
import './index.scss';
import React, { useState, useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
@@ -26,6 +26,7 @@ function InitTabCtx (props, ref) {
const { inputValue, onDebuggerCode } = props;
+ console.log('default value', inputValue);
useImperativeHandle(ref, () => ({
handleTestCodeFormSubmit: (cb) => {
// console.log('父组件调用我啦~~~~~~~~~');
@@ -33,6 +34,10 @@ function InitTabCtx (props, ref) {
}
}));
+ useEffect(() => {
+ console.log('初始值: ========', props);
+ }, [props]);
+
// 渲染文本提示信息
const renderText = () => (请在这里添加测试用例,点击“调试代码”时将从这里读取输入来测试你的代码...);
// 渲染表单信息
diff --git a/public/react/src/modules/developer/components/initTabCtx/index.scss b/public/react/src/modules/developer/components/initTabCtx/index.scss
index 449db1d2a..5427aa374 100644
--- a/public/react/src/modules/developer/components/initTabCtx/index.scss
+++ b/public/react/src/modules/developer/components/initTabCtx/index.scss
@@ -49,7 +49,8 @@
}
.input_textarea_style{
- background:rgba(30,30,30,1) !important;
+ // background:rgba(30,30,30,1) !important;
+ background:rgba(7,15,25,1) !important;
color: #fff;
border-color: transparent;
outline: none;
diff --git a/public/react/src/modules/developer/components/monacoSetting/index.js b/public/react/src/modules/developer/components/monacoSetting/index.js
index a101819e0..c1215f9af 100644
--- a/public/react/src/modules/developer/components/monacoSetting/index.js
+++ b/public/react/src/modules/developer/components/monacoSetting/index.js
@@ -4,10 +4,11 @@
* @Github:
* @Date: 2019-11-25 17:50:33
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-06 16:51:48
+ * @LastEditTime: 2019-12-19 15:26:46
*/
import React, { useState } from 'react';
import { fromStore, toStore } from 'educoder';
+import { Icon } from 'antd';
// import { Select } from 'antd';
// const { Option } = Select;
const SettingDrawer = (props) => {
diff --git a/public/react/src/modules/developer/components/myMonacoEditor/index.js b/public/react/src/modules/developer/components/myMonacoEditor/index.js
index 60f54ee0a..df3c2b5e3 100644
--- a/public/react/src/modules/developer/components/myMonacoEditor/index.js
+++ b/public/react/src/modules/developer/components/myMonacoEditor/index.js
@@ -4,12 +4,12 @@
* @Github:
* @Date: 2019-11-27 15:02:52
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 16:16:56
+ * @LastEditTime: 2019-12-19 17:52:31
*/
import './index.scss';
import React, { useState, useRef, useEffect } from 'react';
-import { Drawer, Modal } from 'antd';
-import { fromStore } from 'educoder';
+import { Drawer, Modal, Icon, Badge } from 'antd';
+import { fromStore, CNotificationHOC } from 'educoder';
import { connect } from 'react-redux';
import MonacoEditor from '@monaco-editor/react';
import SettingDrawer from '../../components/monacoSetting';
@@ -19,16 +19,25 @@ import MyIcon from '../../../../common/components/MyIcon';
// import actions from '../../../../redux/actions';
const { fontSetting, opacitySetting } = CONST;
+const maps = {
+ 'c': 'main.c',
+ 'c++': 'main.cc',
+ 'java': 'main.java',
+ 'pythone': 'main.py'
+};
function MyMonacoEditor (props, ref) {
const {
+ notice,
language,
identifier,
+ hadCodeUpdate,
showOrHideControl,
// saveUserInputCode,
onCodeChange,
- onRestoreInitialCode
+ onRestoreInitialCode,
+ onUpdateNotice
} = props;
const [showDrawer, setShowDrawer] = useState(false); // 控制配置滑框
@@ -50,7 +59,7 @@ function MyMonacoEditor (props, ref) {
}, [props]);
useEffect(() => {
- setHeight(showOrHideControl ? 'calc(100% - 382px)' : 'calc(100% - 56px)');
+ setHeight(showOrHideControl ? 'calc(100% - 378px)' : 'calc(100% - 56px)');
}, [showOrHideControl]);
// 控制侧边栏设置的显示
@@ -93,28 +102,51 @@ function MyMonacoEditor (props, ref) {
// 恢复初始代码
const handleRestoreCode = () => {
- Modal.confirm({
+ props.confirm({
+ title: '提示',
content: '确定要恢复代码吗?',
- okText: '确定',
- cancelText: '取消',
onOk () {
onRestoreInitialCode && onRestoreInitialCode();
}
})
+ // Modal.confirm({
+ // content: '确定要恢复代码吗?',
+ // okText: '确定',
+ // cancelText: '取消',
+ // onOk () {
+ // onRestoreInitialCode && onRestoreInitialCode();
+ // }
+ // })
+ }
+
+ const handleUpdateNotice = () => {
+ if (props.notice) {
+ onUpdateNotice && onUpdateNotice();
+ }
}
const renderRestore = identifier ? (
) : '';
+
+ // lex_has_save ${hadCodeUpdate} ? : ''
+ const _classnames = hadCodeUpdate ? `flex_strict flex_has_save` : 'flex_strict';
return (
{/* 未保存时 ? '学员初始代码文件' : main.x */}
- {identifier ? '' : '学员初始代码文件'}
- {identifier ? '已保存' : ''}
+ {identifier ? language ? maps[language.toLowerCase()] : '' : '学员初始代码文件'}
+ {identifier ? '已保存' : ''}
+
+
+
{renderRestore}
- {/* */}
@@ -161,4 +192,4 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(
mapStateToProps,
mapDispatchToProps
-)(MyMonacoEditor);
+)(CNotificationHOC() (MyMonacoEditor));
diff --git a/public/react/src/modules/developer/components/myMonacoEditor/index.scss b/public/react/src/modules/developer/components/myMonacoEditor/index.scss
index 7a7c4f030..61689b51e 100644
--- a/public/react/src/modules/developer/components/myMonacoEditor/index.scss
+++ b/public/react/src/modules/developer/components/myMonacoEditor/index.scss
@@ -1,19 +1,17 @@
.monaco_editor_area{
height: 100%;
- background-color: rgba(30,30,30,1);
+ background-color: rgba(7,15,25,1);
.code_title{
display: flex;
align-items: center;
- // justify-content: space-between;
- // background: #000;
- // background: #333333;
- background-color: rgba(48,48,48,1);
+ background-color: rgba(18,28,36,1);
color: #fff;
height: 56px;
padding: 0 30px;
.flex_strict{
flex: 1;
}
+
.flex_normal{
color: #E51C24;
cursor: pointer;
@@ -25,21 +23,21 @@
.flex_strict,
.flex_normal,
.code-icon{
- color: #888;
+ color: #666;
}
}
}
.setting_drawer{
- // .ant-drawer-body{
- // // height: calc(100vh - 120px);
- // // overflow-y: auto;
- // }
+ .ant-drawer-close{
+ color: #ffffff;
+ }
.ant-drawer-content{
top: 120px;
bottom: 56px;
height: calc(100vh - 176px);
- background: #333333;
+ // background: #333333;
+ background: rgba(7,15,25,1);
color: #fff;
.setting_h2{
color: #fff;
@@ -57,4 +55,17 @@
color: #fff;
}
}
+}
+
+.flex_has_save{
+ // animation: blink 3s line 3;
+ animation-name: blink;
+ animation-duration: .4s;
+ animation-iteration-count: 3;
+}
+
+@keyframes blink{
+ 50% {
+ color: #fff;
+ }
}
\ No newline at end of file
diff --git a/public/react/src/modules/developer/newOrEditTask/index.js b/public/react/src/modules/developer/newOrEditTask/index.js
index bc905fdf7..1c451c669 100644
--- a/public/react/src/modules/developer/newOrEditTask/index.js
+++ b/public/react/src/modules/developer/newOrEditTask/index.js
@@ -13,11 +13,11 @@ import { Button, Modal } from 'antd';
import LeftPane from './leftpane';
import RightPane from './rightpane';
import { withRouter } from 'react-router';
-import { toStore } from 'educoder';
+import { toStore, CNotificationHOC } from 'educoder';
import UserInfo from '../components/userInfo';
// import RightPane from './rightpane/index';
import actions from '../../../redux/actions';
-import {ModalConfirm} from '../../../common/components/ModalConfirm';
+// import {ModalConfirm} from '../../../common/components/ModalConfirm';
const NewOrEditTask = (props) => {
const {
@@ -69,10 +69,13 @@ const NewOrEditTask = (props) => {
// 模拟挑战
const imitationChallenge = () => {
+ // 调用 start 接口, 成功后跳转到模拟页面
+ startProgramQuestion(identifier, props);
}
// 开始挑战
const startChallenge = () => {
- // 调用 start 接口, 成功后跳转到模拟页面
+ // 调用 start 接口, 成功后跳转到开启实战
+ // TODO
startProgramQuestion(identifier, props);
}
@@ -82,27 +85,38 @@ const NewOrEditTask = (props) => {
props.clearOJFormStore();
// 清空描述信息
toStore('oj_description', '');
- setInterval(function () {
- props.history.push('/problems');
- }, 500);
+ props.history.push('/problems');
}
// 发布
const handleClickPublish = () => {
- ModalConfirm('提示', (发布后即可应用到自己管理的课堂
是否确认发布?
), () => {
- changePublishLoadingStatus(true);
- handlePublish(props, 'publish');
+ // ModalConfirm('提示', (发布后即可应用到自己管理的课堂
是否确认发布?
), () => {
+ // changePublishLoadingStatus(true);
+ // handlePublish(props, 'publish');
+ // });
+ props.confirm({
+ title: '提示',
+ content: (发布后即可应用到自己管理的课堂
是否确认发布?
),
+ onOk () {
+ changePublishLoadingStatus(true);
+ handlePublish(props, 'publish');
+ }
});
-
-
}
// 撤销发布
const handleClickCancelPublish = () => {
- ModalConfirm('提示', (是否确认撤销发布?
), () => {
- changePublishLoadingStatus(true);
- handleCancelPublish(props, identifier);
+ // ModalConfirm('提示', (是否确认撤销发布?
), () => {
+ // changePublishLoadingStatus(true);
+ // handleCancelPublish(props, identifier);
+ // });
+ props.confirm({
+ title: '提示',
+ content: ((是否确认撤销发布?
)),
+ onOk () {
+ changePublishLoadingStatus(true);
+ handleCancelPublish(props, identifier);
+ }
});
-
}
// 取消保存/取消按钮
@@ -125,6 +139,7 @@ const NewOrEditTask = (props) => {
const renderPubOrFight = () => {
const pubButton = isPublish
? (
- {/*
-
-
-
-
*/}
);
diff --git a/public/react/src/modules/developer/studentStudy/leftpane/index.scss b/public/react/src/modules/developer/studentStudy/leftpane/index.scss
index 9e6f019f6..1582c033f 100644
--- a/public/react/src/modules/developer/studentStudy/leftpane/index.scss
+++ b/public/react/src/modules/developer/studentStudy/leftpane/index.scss
@@ -13,6 +13,8 @@
bottom: 0px;
height: 56px;
width: 100%;
+ box-sizing: border-box;
+ border-top: 1px solid rgba(244,244,244,1);
// background: pink;
padding: 0 30px;
// background-color: rgba(250,250,250,1);
@@ -79,4 +81,9 @@
margin-right: 5px;
}
}
+}
+
+.add_editor_list_area{
+ box-sizing: border-box;
+ border-bottom: 1px solid rgba(244,244,244,1);
}
\ No newline at end of file
diff --git a/public/react/src/modules/developer/studentStudy/leftpane/taskDescription/index.js b/public/react/src/modules/developer/studentStudy/leftpane/taskDescription/index.js
index 28fe51765..981770b8c 100644
--- a/public/react/src/modules/developer/studentStudy/leftpane/taskDescription/index.js
+++ b/public/react/src/modules/developer/studentStudy/leftpane/taskDescription/index.js
@@ -4,14 +4,15 @@
* @Github:
* @Date: 2019-11-27 09:49:30
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-09 19:21:55
+ * @LastEditTime: 2019-12-19 09:22:52
*/
import '../index.scss';
import React from 'react';
import { Tag } from 'antd';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
-import QuillEditor from '../../../quillEditor';
+import QuillForEditor from '../../../../../common/quillForEditor';
+
import CONST from '../../../../../constants';
const {tagBackground, diffText} = CONST;
@@ -36,22 +37,15 @@ const TaskDescription = (props) => {
出题者:
- {username}
+ {username}
-
- {/* */}
- {/* */}
)
}
diff --git a/public/react/src/modules/developer/studentStudy/rightpane/index.js b/public/react/src/modules/developer/studentStudy/rightpane/index.js
index 669a8a693..5661ea32e 100644
--- a/public/react/src/modules/developer/studentStudy/rightpane/index.js
+++ b/public/react/src/modules/developer/studentStudy/rightpane/index.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-27 14:59:51
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 16:50:40
+ * @LastEditTime: 2019-12-19 15:06:49
*/
import React, { useState, useEffect } from 'react';
import {connect} from 'react-redux';
@@ -20,8 +20,11 @@ const RightPane = (props) => {
submitUserCode,
input,
hack,
+ notice,
updateCode,
+ hadCodeUpdate,
editor_code,
+ updateNotice,
saveUserInputCode,
restoreInitialCode,
saveUserCodeForInterval
@@ -48,7 +51,8 @@ const RightPane = (props) => {
// 代码块内容变化时
const handleCodeChange = (code) => {
// 保存用户提交的代码块
- console.log(code);
+ // console.log(code);
+ setEditorCode(code);
if (!timer) {
timer = setInterval(() => {
clearInterval(timer);
@@ -69,13 +73,21 @@ const RightPane = (props) => {
restoreInitialCode(identifier, '恢复初始代码成功');
}
+ // 更新代码
+ const handleUpdateNotice = () => {
+ updateNotice && updateNotice();
+ };
+
return (
{
const mapStateToProps = (state) => {
- const {user_program_identifier, hack, userTestInput, editor_code} = state.ojForUserReducer;
+ const {
+ user_program_identifier,
+ hack,
+ userTestInput,
+ editor_code,
+ notice,
+ hadCodeUpdate
+ } = state.ojForUserReducer;
// const { language, code } = hack;
return {
hack,
+ notice,
+ hadCodeUpdate,
editor_code,
input: userTestInput,
submitInput: hack.input,
diff --git a/public/react/src/redux/actions/actionTypes.js b/public/react/src/redux/actions/actionTypes.js
index 55aa42799..1129a3235 100644
--- a/public/react/src/redux/actions/actionTypes.js
+++ b/public/react/src/redux/actions/actionTypes.js
@@ -22,7 +22,8 @@ const types = {
SAVE_OJ_FORM: 'SAVE_OJ_FORM', // 保存表单
ADD_TEST_CASE: 'ADD_TEST_CASE', // 添加测试用例
DELETE_TEST_CASE: 'DELETE_TEST_CASE', // 删除测试用例
- SAVE_TEST_CASE: '保存测试用例', // 保存测试用例
+ SAVE_TEST_CASE: 'SAVE_TEST_CASE', // 保存测试用例
+ SAVE_USE_TEST_CASE_VALUE: 'SAVE_USE_TEST_CASE_VALUE', // 用户自定义测试用例值
CLEAR_JSFORM_STORE: 'CLEAR_JSFORM_STORE', // 清空测试用例
SAVE_EDIT_OJ_FORM_AND_TEST_CASE: 'SAVE_EDIT_OJ_FORM_AND_TEST_CASE', // 保存根据id获取的表单及测试用例值
TEST_CODE_STATUS: 'TEST_CODE_STATUS', // 代码调试状态
@@ -57,7 +58,9 @@ const types = {
SAVE_JUPYTER_INFO: 'SAVE_JUPYTER_INFO', // 保存 jupyter 信息
CHANGE_JUPYTER_URL_STATE: 'CHANGE_JUPYTER_URL_STATE', // 获取url返回的状态值
SAVE_JUPYTER_TPI: 'SAVE_JUPYTER_TPI', // 保存 jupyter tpi
- CHANGE_JUPYTER_CURRENT_PAGE: 'CHANGE_JUPYTER_CURRENT_PAGE'
+ CHANGE_JUPYTER_CURRENT_PAGE: 'CHANGE_JUPYTER_CURRENT_PAGE',
+ SAVE_NOTICE_COUNT: 'SAVE_NOTICE_COUNT', // 保存代码块是否更新
+ AUTO_UPDATE_CODE: 'AUTO_UPDATE_CODE', // 自动更新代码
}
export default types;
diff --git a/public/react/src/redux/actions/ojForUser.js b/public/react/src/redux/actions/ojForUser.js
index 79e2045d6..9cb814d80 100644
--- a/public/react/src/redux/actions/ojForUser.js
+++ b/public/react/src/redux/actions/ojForUser.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-27 13:42:11
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 16:49:42
+ * @LastEditTime: 2019-12-19 15:11:56
*/
import types from "./actionTypes";
import { Base64 } from 'js-base64';
@@ -82,6 +82,20 @@ export const getUserProgramDetail = (identifier, type) => {
payload: data
});
}
+ // 保存默认测试用例
+ dispatch({
+ type: types.SAVE_USE_TEST_CASE_VALUE,
+ payload: data.test_case || {}
+ });
+ // 代码是否更新
+ let _modify_code = false;
+ if (data.hack) {
+ _modify_code = data.hack.modify_code;
+ }
+ dispatch({
+ type: types.SAVE_NOTICE_COUNT,
+ payload: _modify_code
+ })
// 保存用户登录信息
dispatch({
type: types.SAVE_USER_INFO,
@@ -94,13 +108,29 @@ export const getUserProgramDetail = (identifier, type) => {
export const saveUserCodeForInterval = (identifier, code) => {
return (dispatch) => {
+ dispatch({
+ type: types.AUTO_UPDATE_CODE,
+ payload: true
+ });
fetchUpdateCode(identifier, {
code: Base64.encode(code)
}).then(res => {
if (res.data.status === 401) {
return;
};
+
+ setTimeout(() => {
+ dispatch({
+ type: types.AUTO_UPDATE_CODE,
+ payload: false
+ })
+ }, 1000);
console.log('代码保存成功', res);
+ }).catch(() => {
+ dispatch({
+ type: types.AUTO_UPDATE_CODE,
+ payload: false
+ })
});
}
}
@@ -259,7 +289,11 @@ export const debuggerCode = (identifier,value, type) => {
}).catch(() => {
dispatch({
type: types.TEST_CODE_STATUS,
- payload: ''
+ payload: 'error'
+ });
+ dispatch({
+ type: types.LOADING_STATUS,
+ payload: false
});
dispatch({
type: types.SUBMIT_LOADING_STATUS,
@@ -400,6 +434,10 @@ export const restoreInitialCode = (identifier, msg) => {
message: '提示',
description: msg
});
+ dispatch({
+ type: types.SAVE_NOTICE_COUNT,
+ payload: false
+ });
}
});
}
@@ -412,3 +450,6 @@ export const saveEditorCodeForDetail = (code) => {
payload: code
}
}
+
+// 更新通知状态
+
diff --git a/public/react/src/redux/actions/ojForm.js b/public/react/src/redux/actions/ojForm.js
index 3eaac9023..740bad895 100644
--- a/public/react/src/redux/actions/ojForm.js
+++ b/public/react/src/redux/actions/ojForm.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-20 16:35:46
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 16:56:22
+ * @LastEditTime: 2019-12-19 17:20:48
*/
import types from './actionTypes';
import CONST from '../../constants';
@@ -15,7 +15,7 @@ import {
cancelPublicTask
} from '../../services/ojService';
import { Base64 } from 'js-base64';
-import { message, notification, Modal } from 'antd';
+import { notification } from 'antd';
import { toStore } from 'educoder';
const { jcLabel } = CONST;
// 表单字段映射
@@ -160,7 +160,7 @@ export const validateOjForm = (props, type) => {
if (testCases.length === 0) {
hasSuccess = false;
notification['error']({
- message: '必填',
+ message: '提示',
description: '测试用例必须输入!'
});
}
@@ -200,7 +200,7 @@ export const validateOjForm = (props, type) => {
let paramsObj = {};
const hack = { // 编程题干
name,
- description,
+ description: JSON.stringify(description),
difficult,
category,
'open_or_not': openOrNot,
@@ -449,9 +449,13 @@ export const testCaseInputChange = (value, index) => {
let validate = emptyValidate('input', value)['input'];
if (!validate.errMsg) {
// 唯一性校验
+ let _errMsg = '';
const {testCases} = getState().ojFormReducer;
const bool = testCases.some((item, i) => {
if (i !== index) {
+ if (item['input'] === value) {
+ _errMsg=`与测试用例${index}的输入值重复了,请重新填写`;
+ }
return item['input'] === value;
} else {
return false;
@@ -460,7 +464,7 @@ export const testCaseInputChange = (value, index) => {
if (bool) {
validate = {
validateStatus: 'error',
- errMsg: '输入值必须唯一'
+ errMsg: _errMsg
};
}
}
@@ -492,8 +496,12 @@ export const testCaseOutputChange = (value, index) => {
if (!validate.errMsg) {
// 唯一性校验
const {testCases} = getState().ojFormReducer;
+ let _errMsg = '';
const bool = testCases.some((item, i) => {
if (i !== index) {
+ if (item['output'] === value) {
+ _errMsg=`与测试用例${index}的输入值重复了,请重新填写`;
+ }
return item['output'] === value;
} else {
return false;
@@ -502,7 +510,7 @@ export const testCaseOutputChange = (value, index) => {
if (bool) {
validate = {
validateStatus: 'error',
- errMsg: '输入值必须唯一'
+ errMsg: _errMsg
};
}
}
diff --git a/public/react/src/redux/reducers/ojForUserReducer.js b/public/react/src/redux/reducers/ojForUserReducer.js
index 6520f8954..60e7e6fd1 100644
--- a/public/react/src/redux/reducers/ojForUserReducer.js
+++ b/public/react/src/redux/reducers/ojForUserReducer.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-27 13:41:48
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-10 10:09:59
+ * @LastEditTime: 2019-12-19 14:49:03
*/
import types from "../actions/actionTypes";
import { Base64 } from 'js-base64';
@@ -21,7 +21,9 @@ const initialState = {
userTestInput: '', // 用户自定义输入值
recordDetail: {}, // 根据id号获取的记录详情
hack_identifier: '', // 用户界面编辑时
- editor_code: '' // 保存编辑代码
+ editor_code: '', // 保存编辑代码
+ notice: false, // 通知
+ hadCodeUpdate: false, // 更新代码
};
const ojForUserReducer = (state = initialState, action) => {
@@ -36,7 +38,8 @@ const ojForUserReducer = (state = initialState, action) => {
const { hack, test_case } = action.payload;
const { code }= hack;
let tempCode = Base64.decode(code)
- Object.assign(hack, {code: tempCode});
+ let tempDesc = JSON.parse(hack.description);
+ Object.assign(hack, {code: tempCode, description: tempDesc});
return {
...state,
hack: Object.assign({}, hack),
@@ -121,6 +124,21 @@ const ojForUserReducer = (state = initialState, action) => {
...state,
editor_code: action.payload
}
+ case types.SAVE_USE_TEST_CASE_VALUE:
+ return {
+ ...state,
+ userTestInput: action.payload.input
+ }
+ case types.SAVE_NOTICE_COUNT:
+ return {
+ ...state,
+ notice: action.payload
+ };
+ case types.AUTO_UPDATE_CODE:
+ return {
+ ...state,
+ hadCodeUpdate: action.payload
+ };
default:
return state;
}
diff --git a/public/react/src/redux/reducers/ojFormReducer.js b/public/react/src/redux/reducers/ojFormReducer.js
index c2ba0f4d8..39af4a5f8 100644
--- a/public/react/src/redux/reducers/ojFormReducer.js
+++ b/public/react/src/redux/reducers/ojFormReducer.js
@@ -4,7 +4,7 @@
* @Github:
* @Date: 2019-11-20 16:40:32
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 11:54:35
+ * @LastEditTime: 2019-12-17 16:19:04
*/
import { Base64 } from 'js-base64';
import types from '../actions/actionTypes';
@@ -176,7 +176,7 @@ const ojFormReducer = (state = initialState, action) => {
const currentOjForm = {
name, // 任务名称
language,
- description,
+ description: JSON.parse(description),
difficult,
category,
openOrNot: 1,
diff --git a/public/react/src/services/ojService.js b/public/react/src/services/ojService.js
index e13b66397..373805b73 100644
--- a/public/react/src/services/ojService.js
+++ b/public/react/src/services/ojService.js
@@ -4,10 +4,11 @@
* @Github:
* @Date: 2019-11-20 10:55:38
* @LastEditors: tangjiang
- * @LastEditTime: 2019-12-13 17:03:47
+ * @LastEditTime: 2019-12-17 14:10:37
*/
import axios from 'axios';
+import { func } from 'prop-types';
export async function fetchOJList (params) {
console.log('传递的参数: ', params);
@@ -125,3 +126,15 @@ export async function fetchUserInfoForNew () {
const url = `/problems/new.json`;
return axios.get(url);
}
+
+// 文件上传
+export async function fetchUploadImage (file) {
+ const url = `/attachments.json`;
+ return axios.post(url, file)
+}
+
+// 根据id号获取图片url
+export async function fetchUploadImageUrl (id) {
+ const url = `/attachments/${id}`;
+ return axios.get(url);
+}