Merge branch 'develop' of http://bdgit.educoder.net/Hjqreturn/educoder into develop
commit
4009ed9d2c
@ -0,0 +1,14 @@
|
||||
class LibrariesController < ApplicationController
|
||||
include PaginateHelper
|
||||
|
||||
def index
|
||||
default_sort('updated_at', 'desc')
|
||||
|
||||
@items = ItemBankQuery.call(params)
|
||||
@items = paginate courses.includes(:school, :students, :attachments, :homework_commons, teacher: :user_extension)
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
end
|
||||
end
|
@ -0,0 +1,3 @@
|
||||
class ItemAnalysis < ApplicationRecord
|
||||
belongs_to :item_bank
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
class ItemBank < ApplicationRecord
|
||||
# difficulty: 1 简单 2 适中 3 困难
|
||||
# item_type: 0 单选 1 多选 2 判断 3 填空 4 简答 5 实训 6 编程
|
||||
enum item_type: { SINGLE: 0, MULTIPLE: 1, JUDGMENT: 2, COMPLETION: 3, SUBJECTIVE: 4, PRACTICAL: 5, PROGRAM: 6 }
|
||||
|
||||
belongs_to :user
|
||||
|
||||
has_one :item_analysis, dependent: :destroy
|
||||
has_many :item_choices, dependent: :destroy
|
||||
has_many :item_baskets, dependent: :destroy
|
||||
end
|
@ -0,0 +1,4 @@
|
||||
class ItemBasket < ApplicationRecord
|
||||
belongs_to :item_bank
|
||||
belongs_to :user
|
||||
end
|
@ -0,0 +1,3 @@
|
||||
class ItemChoice < ApplicationRecord
|
||||
belongs_to :item_bank
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
json.courses @courses do |course|
|
||||
json.(course, :id, :name, :is_end, :evaluating_count)
|
||||
json.teachers course.teacher_users.map(&:real_name).join('、')
|
||||
json.student_count @student_count.fetch(course.id, 0)
|
||||
json.shixun_work_count @shixun_work_count.fetch(course.id, 0)
|
||||
json.attachment_count @attachment_count.fetch(course.id, 0)
|
||||
json.message_count @message_count.fetch(course.id, 0)
|
||||
json.other_work_count @exercise_count.fetch(course.id, 0) + @poll_count.fetch(course.id, 0) + @other_work_count.fetch(course.id, 0)
|
||||
json.activity_time @active_time[course.id]&.strftime('%Y-%m-%d %H:%M')
|
||||
end
|
||||
json.course_count @course_count
|
@ -0,0 +1,10 @@
|
||||
json.teachers @students do |student|
|
||||
json.login student.login
|
||||
json.name student.real_name
|
||||
json.student_id student.student_id
|
||||
json.shixun_count @shixun_count.fetch(student.id, 0)
|
||||
json.study_shixun_count @study_shixun_count.fetch(student.id, 0)
|
||||
json.grade student.grade
|
||||
json.experience student.experience
|
||||
end
|
||||
# json.student_count @student_count
|
@ -0,0 +1,11 @@
|
||||
json.teachers @teachers do |teacher|
|
||||
json.login teacher['login']
|
||||
json.name teacher['real_name']
|
||||
json.course_count teacher['course_count']
|
||||
json.shixun_work_count teacher['shixun_work_count']
|
||||
json.un_shixun_work_count teacher['un_shixun_work_count']
|
||||
json.student_count teacher['student_count']
|
||||
json.complete_rate teacher['complete_rate']
|
||||
json.publish_shixun_count teacher['publish_shixun_count']
|
||||
end
|
||||
# json.teacher_count @teacher_count
|
@ -1,6 +1,6 @@
|
||||
json.partial! 'shixuns/right', locals: { shixun: @shixun }
|
||||
|
||||
json.follow follow?(@shixun.owner, User.current)
|
||||
json.fans_count @fans_count
|
||||
json.followed_count @followed_count
|
||||
# json.follow follow?(@shixun.owner, User.current)
|
||||
# json.fans_count @fans_count
|
||||
# json.followed_count @followed_count
|
||||
json.user_shixuns_count @user_own_shixuns
|
||||
|
@ -0,0 +1,16 @@
|
||||
class CreateItemBanks < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :item_banks do |t|
|
||||
t.text :name
|
||||
t.references :curriculum, index: true
|
||||
t.references :curriculum_direction, index: true
|
||||
t.integer :item_type
|
||||
t.integer :difficulty
|
||||
t.references :user, index: true
|
||||
t.boolean :public
|
||||
t.integer :quotes
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
class CreateItemAnalyses < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :item_analyses do |t|
|
||||
t.references :item_bank, index: true
|
||||
t.text :analysis
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
class CreateItemChoices < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :item_choices do |t|
|
||||
t.references :item_bank, index: true
|
||||
t.text :choice_text
|
||||
t.boolean :is_answer
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
class CreateItemBaskets < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :item_baskets do |t|
|
||||
t.references :item_bank, index: true
|
||||
t.references :user, index: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,10 @@
|
||||
class ModifyDescriptionForHacks < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
change_column :hacks, :description, :longtext
|
||||
change_column :hack_codes, :code, :longtext
|
||||
change_column :hack_user_lastest_codes, :code, :longtext
|
||||
change_column :hack_user_codes, :code, :longtext
|
||||
change_column :hack_user_debugs, :code, :longtext
|
||||
|
||||
end
|
||||
end
|
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,51 @@
|
||||
//用于嵌入到jupyter pod中的js
|
||||
//guange 2019.12.18
|
||||
|
||||
var timebool=false;
|
||||
window.onload=function(){
|
||||
console.log("开始发送消息了");
|
||||
timebool=true;
|
||||
// runEvery10Sec();
|
||||
}
|
||||
|
||||
function runEvery10Sec() {
|
||||
// 1000 * 10 = 10 秒钟
|
||||
// console.log("每隔10秒中一次");
|
||||
require(["base/js/namespace"],function(Jupyter) {
|
||||
Jupyter.notebook.save_checkpoint();
|
||||
});
|
||||
window.parent.postMessage('jupytermessage','*');
|
||||
// if(timebool===true){
|
||||
// setTimeout( runEvery10Sec, 1000 * 10 );
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
window.onload=function(){
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.keyCode == 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)){
|
||||
e.preventDefault();
|
||||
console.log("ctrl+s");
|
||||
window.parent.postMessage('jupytermessage','*');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if(e){
|
||||
if(e.data){
|
||||
if(e.data==="stopParent"){
|
||||
//重置停止
|
||||
timebool=false;
|
||||
console.log("父窗口调用停止");
|
||||
}else if(e.data==="clonsParent"){
|
||||
console.log("父窗口调用启动");
|
||||
//取消启动
|
||||
timebool=true;
|
||||
// runEvery10Sec();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 362 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* @Author: your name
|
||||
* @Date: 2019-12-20 11:40:56
|
||||
* @LastEditTime : 2019-12-20 13:38:49
|
||||
* @LastEditors : Please set LastEditors
|
||||
* @Description: In User Settings Edit
|
||||
* @FilePath: /notebook/Users/yangshuming/Desktop/new__educode/educoder/public/react/public/js/jupyter.js
|
||||
*/
|
||||
window.onload=function(){
|
||||
require(["base/js/namespace"],function(Jupyter) {
|
||||
Jupyter.notebook.save_checkpoint();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// //子目标父窗口接收子窗口发送的消息
|
||||
// let message = {type: 'open', link:'需要发送的消息'};
|
||||
//子窗口向父窗口发送消息,消息中包含我们想跳转的链接
|
||||
window.parent.postMessage('jupytermessage','需要发送的消息');
|
||||
|
||||
|
||||
|
||||
// //目标父窗口接收子窗口发送的消息
|
||||
// window.addEventListener('message', (e)=>{
|
||||
// let origin = event.origin || event.originalEvent.origin;
|
||||
// if (origin !== '需要发送的消息') {
|
||||
// return;
|
||||
// }else {
|
||||
// //更换iframe的src,实现iframe页面跳转
|
||||
// 执行方法
|
||||
// }
|
||||
// },false);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
import React, {Component} from "react";
|
||||
import {WordsBtn} from 'educoder';
|
||||
import {Table} from "antd";
|
||||
import {Link,Switch,Route,Redirect} from 'react-router-dom';
|
||||
const echarts = require('echarts');
|
||||
|
||||
|
||||
|
||||
function startechart(data,datanane){
|
||||
var effChart = echarts.init(document.getElementById('shixun_skill_chart'));
|
||||
|
||||
var option = {
|
||||
|
||||
tooltip : {
|
||||
trigger: 'item',
|
||||
formatter: "{d}% <br/>"
|
||||
},
|
||||
legend: {
|
||||
// orient: 'vertical',
|
||||
// top: 'middle',
|
||||
bottom: 50,
|
||||
left: 'center',
|
||||
data: datanane
|
||||
},
|
||||
series : [
|
||||
{
|
||||
type: 'pie',
|
||||
radius : '65%',
|
||||
center: ['50%', '35%'],
|
||||
selectedMode: 'single',
|
||||
data:data,
|
||||
itemStyle: {
|
||||
emphasis: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
effChart.setOption(option);
|
||||
}
|
||||
class Colleagechart extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
startechart(this.props.data,this.props.datanane)
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.data!= this.props.data) {
|
||||
startechart(this.props.data,this.props.datanane)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let {data}=this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div
|
||||
style={{ width:'100%',height:'600px'}}
|
||||
id="shixun_skill_chart">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Colleagechart;
|
@ -0,0 +1,149 @@
|
||||
import React, {Component} from "react";
|
||||
import {WordsBtn} from 'educoder';
|
||||
import {Table} from "antd";
|
||||
import {Link,Switch,Route,Redirect} from 'react-router-dom';
|
||||
const echarts = require('echarts');
|
||||
|
||||
|
||||
|
||||
function startechart(names, values){
|
||||
var effChart = echarts.init(document.getElementById('shixun_skill_charts'));
|
||||
|
||||
var Color = ['#962e66', '#623363', '#CCCCCC', '#9A9A9A', '#FF8080', '#FF80C2', '#B980FF', '#80B9FF', '#6FE9FF', '#4DE8B4', '#F8EF63', '#FFB967'];
|
||||
|
||||
var option = {
|
||||
backgroundColor: '#fff',
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '8%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
show: "true",
|
||||
trigger: 'item',
|
||||
formatter: '{c0}',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)', // 背景
|
||||
padding: [8, 10], //内边距
|
||||
extraCssText: 'box-shadow: 0 0 3px rgba(255, 255, 255, 0.4);', //添加阴影
|
||||
axisPointer: { // 坐标轴指示器,坐标轴触发有效
|
||||
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#CCCCCC'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#CCCCCC'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#656565',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '12'
|
||||
},
|
||||
formatter: '{value}'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#cccccc'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitArea: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
inside: false,
|
||||
textStyle: {
|
||||
color: '#656565',
|
||||
fontWeight: 'normal',
|
||||
fontSize: '12'
|
||||
}
|
||||
},
|
||||
data: names
|
||||
},
|
||||
series: [{
|
||||
name: '',
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
show: true,
|
||||
color: function(params) {
|
||||
return Color[params.dataIndex]
|
||||
},
|
||||
barBorderRadius: 50,
|
||||
borderWidth: 0,
|
||||
borderColor: '#333'
|
||||
}
|
||||
},
|
||||
barGap: '0%',
|
||||
barCategoryGap: '50%',
|
||||
data: values
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
effChart.setOption(option);
|
||||
}
|
||||
class Colleagechartzu extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
startechart(this.props.data,this.props.datavule)
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.data!= this.props.data) {
|
||||
startechart(this.props.data,this.props.datavule)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let {data}=this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div
|
||||
style={{ width:'100%',height:'600px'}}
|
||||
id="shixun_skill_charts">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Colleagechartzu;
|
@ -0,0 +1,213 @@
|
||||
.yslstatistic-header {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
background-image: url('/images/educoder/statistics.jpg');
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.yslborder{
|
||||
border: 1px solid;
|
||||
}
|
||||
.yslstatistic-header-title{
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4CACFF;
|
||||
font-size: 32px;
|
||||
}
|
||||
.yslstatistic-header-content{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.yslstatistic-header-item{
|
||||
margin-bottom: 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
.yslstatistic-header-item-label{
|
||||
color: #989898;
|
||||
}
|
||||
|
||||
.yslstatistic-base-item-label{
|
||||
width: 217px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
color: #686868;
|
||||
background: #F5F5F5;
|
||||
border-top: 1px solid #EBEBEB;
|
||||
}
|
||||
.yslstatistic-base-item-labels{
|
||||
width: 217px;
|
||||
text-align: center;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #EBEBEB;
|
||||
border-bottom: 1px solid #EBEBEB;
|
||||
}
|
||||
.yslstatistic-base-item-labelsp{
|
||||
color: #000000;
|
||||
font-size: 24px;
|
||||
}
|
||||
.yslstatistic-base-item-labelsspan{
|
||||
color: #000000;
|
||||
margin-left: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.jibenshiyong100{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.yslstatistic-header-item-content{
|
||||
font-size: 24px;
|
||||
}
|
||||
/* 中间居中 */
|
||||
.intermediatecenter{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 简单居中 */
|
||||
.intermediatecenterysls{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.spacearound{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
}
|
||||
.spacebetween{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
/* 头顶部居中 */
|
||||
.topcenter{
|
||||
display: -webkit-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* x轴正方向排序 */
|
||||
/* 一 二 三 四 五 六 七 八 */
|
||||
.sortinxdirection{
|
||||
display: flex;
|
||||
flex-direction:row;
|
||||
}
|
||||
/* x轴反方向排序 */
|
||||
/* 八 七 六 五 四 三 二 一 */
|
||||
.xaxisreverseorder{
|
||||
display: flex;
|
||||
flex-direction:row-reverse;
|
||||
}
|
||||
/* 垂直布局 正方向*/
|
||||
/* 一
|
||||
二
|
||||
三
|
||||
四
|
||||
五
|
||||
六
|
||||
七
|
||||
八 */
|
||||
.verticallayout{
|
||||
display: flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
/* 垂直布局 反方向*/
|
||||
.reversedirection{
|
||||
display: flex;
|
||||
flex-direction:column-reverse;
|
||||
}
|
||||
|
||||
.h4{
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
.ysllinjibenshiyong{
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
padding: 2rem 1.25rem;
|
||||
border-bottom: unset;
|
||||
background:#fff;
|
||||
}
|
||||
.linjibenshiyong{
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
padding: 2rem 1.25rem;
|
||||
border-bottom: unset;
|
||||
background:#fff;
|
||||
box-shadow:0px 6px 12px 0px rgba(0,0,0,0.1);
|
||||
border-radius:2px;
|
||||
}
|
||||
.yslslinjibenshiyong{
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
border-bottom: unset;
|
||||
box-shadow:0px 6px 12px 0px rgba(0,0,0,0.1);
|
||||
border-radius:2px;
|
||||
}
|
||||
.yinyin{
|
||||
background: #fff;
|
||||
box-shadow:0px 6px 12px 0px rgba(0,0,0,0.1);
|
||||
border-radius:2px;
|
||||
}
|
||||
.edu-back-eeee{
|
||||
background:#EEEEEE !important;
|
||||
}
|
||||
.mt-4{
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.statistic-label{
|
||||
padding: 2rem 1.25rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
.mb50{
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
.mt40{
|
||||
margin-top: 40px;
|
||||
}
|
||||
.mb80{
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
.task-hide{overflow:hidden; white-space: nowrap; text-overflow:ellipsis;}
|
||||
a:hover{
|
||||
color:#0056b3;
|
||||
}
|
||||
.color-blue{
|
||||
color: #4CACFF;
|
||||
}
|
||||
|
||||
.color-huang{
|
||||
color:#ffc107 !important
|
||||
}
|
||||
.maxnamewidth105{
|
||||
max-width: 105px;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.maxnamewidth247{
|
||||
max-width: 247px;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
cursor: default;
|
||||
}
|
||||
.maxnamewidth340{
|
||||
max-width: 340px;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
cursor: default;
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* @Description: 评论表单
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-17 17:32:55
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-18 17:51:44
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Form, Button, Input } from 'antd';
|
||||
import QuillForEditor from '../../quillForEditor';
|
||||
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html'
|
||||
const FormItem = Form.Item;
|
||||
|
||||
function CommentForm (props) {
|
||||
|
||||
const {
|
||||
commentCtxChagne,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
form
|
||||
} = props;
|
||||
|
||||
const { getFieldDecorator } = form;
|
||||
const [ctx, setCtx] = useState('');
|
||||
|
||||
const options = [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{header: [1,2,3,false]}],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
['formula']
|
||||
];
|
||||
// const { form: { getFieldDecorator } } = props;
|
||||
const [showQuill, setShowQuill] = useState(false);
|
||||
// 点击输入框
|
||||
const handleInputClick = () => {
|
||||
setShowQuill(true);
|
||||
}
|
||||
// 取消
|
||||
const handleCancle = () => {
|
||||
setShowQuill(false);
|
||||
onCancel && onCancel();
|
||||
}
|
||||
|
||||
// 编辑器内容变化时
|
||||
const handleContentChange = (content) => {
|
||||
setCtx(content);
|
||||
try {
|
||||
const _html = new QuillDeltaToHtmlConverter(content.ops, {}).convert();
|
||||
// props.form.setFieldsValue({'comment': _html.replace(/<\/?[^>]*>/g, '')});
|
||||
props.form.setFieldsValue({'comment': _html});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
// 发送
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
setShowQuill(false);
|
||||
const content = ctx;
|
||||
props.form.setFieldsValue({'comment': ''});
|
||||
setCtx('');
|
||||
console.log(content);
|
||||
onSubmit && onSubmit(content);
|
||||
}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Form>
|
||||
<FormItem>
|
||||
{
|
||||
getFieldDecorator('comment', {
|
||||
rules: [
|
||||
{ required: true, message: '评论内容不能为空'}
|
||||
],
|
||||
})(
|
||||
<Input
|
||||
onClick={handleInputClick}
|
||||
placeholder="说点儿什么~"
|
||||
style={{
|
||||
height: showQuill ? '0px' : '40px',
|
||||
overflow: showQuill ? 'hidden' : 'auto',
|
||||
opacity: showQuill ? 0 : 1,
|
||||
transition: 'all .3s'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<QuillForEditor
|
||||
imgAttrs={{width: '60px', height: '30px'}}
|
||||
wrapStyle={{
|
||||
height: showQuill ? 'auto' : '0px',
|
||||
opacity: showQuill ? 1 : 0,
|
||||
overflow: showQuill ? 'none' : 'hidden',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
style={{ height: '150px', overflowY: 'auto' }}
|
||||
placeholder="说点儿什么~"
|
||||
options={options}
|
||||
value={ctx}
|
||||
onContentChange={handleContentChange}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem style={{ textAlign: 'right' }}>
|
||||
<Button onClick={handleCancle}>取消</Button>
|
||||
<Button onClick={handleSubmit} type="primary" style={{ marginLeft: '10px'}}>发送</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form.create()(CommentForm);
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-18 10:49:46
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-18 11:39:23
|
||||
*/
|
||||
import './index.scss';
|
||||
import React from 'react';
|
||||
import { Icon } from 'antd';
|
||||
function CommentIcon ({
|
||||
type, // 图标类型
|
||||
count, // 评论数
|
||||
iconClick,
|
||||
...props
|
||||
}) {
|
||||
|
||||
// 点击图标
|
||||
const handleSpanClick = () => {
|
||||
iconClick && iconClick();
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`comment_icon_count ${props.className}`} onClick={ handleSpanClick }>
|
||||
<Icon className="comment_icon" type={type} />
|
||||
<span className="comment_count">{ count }</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommentIcon;
|
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* @Description: 评论单列
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-17 17:35:17
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-19 18:02:28
|
||||
*/
|
||||
import './index.scss';
|
||||
import React, { useState } from 'react';
|
||||
import CommentIcon from './CommentIcon';
|
||||
import { getImageUrl, CNotificationHOC } from 'educoder'
|
||||
import { Icon } from 'antd';
|
||||
import moment from 'moment';
|
||||
// import QuillForEditor from '../../quillForEditor';
|
||||
import CommentForm from './CommentForm';
|
||||
|
||||
// import {ModalConfirm} from '../ModalConfirm';
|
||||
function CommentItem ({
|
||||
options,
|
||||
confirm
|
||||
}) {
|
||||
// 显示评论输入框
|
||||
const [showQuill, setShowQuill] = useState(false);
|
||||
// 加载更多评论内容
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
// 箭头方向
|
||||
const [arrow, setArrow] = useState(false);
|
||||
// 删除评论
|
||||
const deleteComment = () => {
|
||||
console.log('删除评论...');
|
||||
confirm({
|
||||
title: '提示',
|
||||
content: (<p>确定要删除该条回复吗?</p>),
|
||||
onOk () {
|
||||
console.log('点击了删除');
|
||||
}
|
||||
});
|
||||
// ModalConfirm('提示', (<p>确定要删除该条回复吗?</p>), () => {
|
||||
// console.log('点击了删除');
|
||||
// });
|
||||
}
|
||||
|
||||
// 评论头像
|
||||
const commentAvatar = (url) => (
|
||||
<img className="item-flex flex-image" src='https://b-ssl.duitang.com/uploads/item/201511/13/20151113110434_kyReJ.jpeg' alt=""/>
|
||||
);
|
||||
|
||||
// 评论信息
|
||||
const commentInfo = () => (
|
||||
<p className="item-header">
|
||||
<span className="item-name">用户名</span>
|
||||
<span className="item-time">{moment(new Date(), 'YYYYMMDD HHmmss').fromNow()}</span>
|
||||
<span className="item-close"><Icon type="close" onClick={deleteComment}/></span>
|
||||
</p>
|
||||
);
|
||||
|
||||
// 评论内容
|
||||
const commentCtx = (ctx) => (
|
||||
<p className="item-ctx">
|
||||
这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容这是评论内容
|
||||
</p>
|
||||
);
|
||||
|
||||
// 加载更多
|
||||
const handleOnLoadMore = () => {
|
||||
if (!arrow) {
|
||||
// 展开所有
|
||||
} else {
|
||||
// 收起
|
||||
}
|
||||
setArrow(!arrow);
|
||||
};
|
||||
|
||||
// 评论追加内容
|
||||
const commentAppend = () => {
|
||||
|
||||
return (
|
||||
<ul className="comment_item_append_list">
|
||||
<li className="comment_item_area">
|
||||
{commentAvatar()}
|
||||
<div className="item-flex item-desc">
|
||||
{commentInfo()}
|
||||
{commentCtx()}
|
||||
</div>
|
||||
</li>
|
||||
<li className="comment_item_area">
|
||||
{commentAvatar()}
|
||||
<div className="item-flex item-desc">
|
||||
{commentInfo()}
|
||||
{commentCtx()}
|
||||
</div>
|
||||
</li>
|
||||
<li className="comment_item_area">
|
||||
{commentAvatar()}
|
||||
<div className="item-flex item-desc">
|
||||
{commentInfo()}
|
||||
{commentCtx()}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li className="comment_item_loadmore" onClick={handleOnLoadMore}>
|
||||
<p className="loadmore-txt">展开其余23条评论</p>
|
||||
<p className="loadmore-icon">
|
||||
<Icon type={!arrow ? 'down' : 'up'}/>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
// 点击图标
|
||||
const handleIconClick = () => {}
|
||||
|
||||
// 点击评论icon
|
||||
const handleClickMessage = () => {
|
||||
setShowQuill(true);
|
||||
}
|
||||
|
||||
// 点击取消
|
||||
const handleClickCancel = () => {
|
||||
setShowQuill(false);
|
||||
}
|
||||
|
||||
// 点击保存
|
||||
const handleClickSubmit = (content) => {
|
||||
// 保存并关闭
|
||||
setShowQuill(false);
|
||||
console.log('获取保存内容', content);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="comment_item_area">
|
||||
{commentAvatar()}
|
||||
<div className="item-flex item-desc">
|
||||
{commentInfo()}
|
||||
{commentCtx()}
|
||||
|
||||
{commentAppend()}
|
||||
|
||||
<div className="comment_icon_area">
|
||||
<CommentIcon className='comment-icon-margin' type="eye" count="100" iconClick={handleIconClick}/>
|
||||
{/* 回复 */}
|
||||
<CommentIcon
|
||||
className='comment-icon-margin'
|
||||
type="message" count="100"
|
||||
iconClick={handleClickMessage}
|
||||
/>
|
||||
{/* 点赞 */}
|
||||
<CommentIcon/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ display: showQuill ? 'block' : 'none'}}
|
||||
className="comment_item_quill">
|
||||
<CommentForm
|
||||
onCancel={handleClickCancel}
|
||||
onSubmit={handleClickSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default CNotificationHOC() (CommentItem);
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* @Description: 评论列表页
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-17 17:34:00
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-18 11:48:09
|
||||
*/
|
||||
import './index.scss';
|
||||
import React from 'react';
|
||||
import CommentItem from './CommentItem';
|
||||
function CommentList ({}) {
|
||||
return (
|
||||
<ul className="comment_list_wrapper">
|
||||
<CommentItem />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentList;
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* @Description: 评论组件
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-17 17:31:33
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-18 11:47:39
|
||||
*/
|
||||
import React from 'react';
|
||||
import CommentForm from './CommentForm';
|
||||
import CommentList from './CommentList';
|
||||
function Comment (props) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CommentForm />
|
||||
<CommentList />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Comment;
|
@ -0,0 +1,111 @@
|
||||
$bdColor: rgba(244,244,244,1);
|
||||
$bgColor: rgba(250,250,250,1);
|
||||
$lh14: 14px;
|
||||
$lh22: 22px;
|
||||
$fz14: 14px;
|
||||
$fz12: 12px;
|
||||
$ml: 20px;
|
||||
|
||||
.comment_list_wrapper{
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid $bdColor;
|
||||
|
||||
.comment_item_area{
|
||||
display: flex;
|
||||
padding: 20px 0;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid $bdColor;
|
||||
.flex-image{
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.item-desc{
|
||||
flex: 1;
|
||||
margin-left: $ml;
|
||||
}
|
||||
.item-header{
|
||||
font-size: $fz14;
|
||||
line-height: $lh14;
|
||||
color: #333;
|
||||
.item-time{
|
||||
font-size: $fz12;
|
||||
line-height: $lh14;
|
||||
margin-left: $ml;
|
||||
}
|
||||
.item-close{
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.item-ctx{
|
||||
line-height: $lh22;
|
||||
font-size: $fz12;
|
||||
color: #333;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.comment_icon_area{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.comment-icon-margin{
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment_item_quill{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
.comment_icon_count{
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
|
||||
.comment_icon{
|
||||
color: #333;
|
||||
}
|
||||
.comment_count{
|
||||
color: #999999;
|
||||
margin-left: 10px;
|
||||
transition: color .3s;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
.comment_icon,
|
||||
.comment_count{
|
||||
color: #5091FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
.comment_item_append_list{
|
||||
position: relative;
|
||||
background-color: $bgColor;
|
||||
border-radius: 5px;
|
||||
padding: 0 15px 10px;
|
||||
margin: 15px 0;
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
bottom: 100%;
|
||||
height: 0;
|
||||
width: 0;
|
||||
content: '';
|
||||
// border: 5px solid transparent;
|
||||
border: 10px solid transparent;
|
||||
border-bottom-color: $bgColor;
|
||||
}
|
||||
|
||||
.comment_item_loadmore{
|
||||
padding-top: 10px;
|
||||
cursor: pointer;
|
||||
.loadmore-txt,
|
||||
.loadmore-icon{
|
||||
color: #999;
|
||||
text-align: center;
|
||||
font-size: $fz12;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* @Description: 重写图片
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-16 15:50:45
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-17 16:44:48
|
||||
*/
|
||||
import Quill from "quill";
|
||||
|
||||
const BlockEmbed = Quill.import('blots/block/embed');
|
||||
|
||||
export default class ImageBlot extends BlockEmbed {
|
||||
|
||||
static create(value) {
|
||||
|
||||
const node = super.create();
|
||||
|
||||
node.setAttribute('alt', value.alt);
|
||||
node.setAttribute('src', value.url);
|
||||
|
||||
if (value.width) {
|
||||
node.setAttribute('width', value.width);
|
||||
}
|
||||
if (value.height) {
|
||||
node.setAttribute('height', value.height);
|
||||
}
|
||||
// 宽度和高度都不存在时,
|
||||
if (!value.width && !value.height) {
|
||||
node.setAttribute('display', 'block');
|
||||
node.setAttribute('width', '100%');
|
||||
}
|
||||
// 给图片添加点击事件
|
||||
node.onclick = () => {
|
||||
value.onClick && value.onClick(value.url);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static value (node) {
|
||||
|
||||
return {
|
||||
alt: node.getAttribute('alt'),
|
||||
url: node.getAttribute('src'),
|
||||
onclick: node.onclick,
|
||||
// width: node.width,
|
||||
// height: node.height,
|
||||
display: node.getAttribute('display')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ImageBlot.blotName = 'image';
|
||||
ImageBlot.tagName = 'img';
|
@ -0,0 +1,47 @@
|
||||
function deepEqual (prev, current) {
|
||||
if (prev === current) { // 基本类型比较,值,类型都相同 或者同为 null or undefined
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!prev && current)
|
||||
|| (prev && !current)
|
||||
|| (!prev && !current)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(prev)) {
|
||||
if (!Array.isArray(current)) return false;
|
||||
if (prev.length !== current.length) return false;
|
||||
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (!deepEqual(current[i], prev[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof current === 'object') {
|
||||
if (typeof prev !== 'object') return false;
|
||||
const prevKeys = Object.keys(prev);
|
||||
const curKeys = Object.keys(current);
|
||||
|
||||
if (prevKeys.length !== curKeys.length) return false;
|
||||
|
||||
prevKeys.sort();
|
||||
curKeys.sort();
|
||||
|
||||
for (let i = 0; i < prevKeys.length; i++) {
|
||||
if (prevKeys[i] !== curKeys[i]) return false;
|
||||
const key = prevKeys[i];
|
||||
if (!deepEqual(prev[key], current[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default deepEqual;
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* @Description: quill 编辑器
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-18 08:49:30
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-20 16:07:37
|
||||
*/
|
||||
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';
|
||||
|
||||
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 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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 设置值
|
||||
useEffect(() => {
|
||||
if (!quill) return
|
||||
const previous = quill.getContents()
|
||||
const current = value
|
||||
|
||||
if (!deepEqual(previous, current)) {
|
||||
setSelection(quill.getSelection())
|
||||
if (typeof value === 'string') {
|
||||
quill.clipboard.dangerouslyPasteHTML(value, 'api')
|
||||
} else {
|
||||
quill.setContents(value)
|
||||
}
|
||||
}
|
||||
}, [quill, value, setQuill]);
|
||||
|
||||
// 清除选择区域
|
||||
useEffect(() => {
|
||||
if (quill && selection) {
|
||||
quill.setSelection(selection)
|
||||
setSelection(null)
|
||||
}
|
||||
}, [quill, selection, setSelection]);
|
||||
|
||||
// 设置placeholder值
|
||||
useEffect(() => {
|
||||
if (!quill || !quill.root) return;
|
||||
quill.root.dataset.placeholder = placeholder;
|
||||
}, [quill, placeholder]);
|
||||
|
||||
// 处理内容变化
|
||||
useEffect(() => {
|
||||
if (!quill) return;
|
||||
if (typeof handleOnChange !== 'function') return;
|
||||
let handler;
|
||||
quill.on(
|
||||
'text-change',
|
||||
(handler = () => {
|
||||
handleOnChange(quill.getContents()); // getContents: 检索编辑器内容
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
quill.off('text-change', handler);
|
||||
}
|
||||
}, [quill, handleOnChange]);
|
||||
|
||||
// 返回结果
|
||||
return (
|
||||
<div className='quill_editor_for_react_area' style={wrapStyle}>
|
||||
<div ref={editorRef} style={style}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuillForEditor;
|
@ -0,0 +1,5 @@
|
||||
.quill_editor_for_react_area{
|
||||
.ql-editing{
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* @Description: 重写图片
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-16 15:50:45
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-17 16:44:48
|
||||
*/
|
||||
import Quill from "quill";
|
||||
|
||||
const BlockEmbed = Quill.import('blots/block/embed');
|
||||
|
||||
export default class ImageBlot extends BlockEmbed {
|
||||
|
||||
static create(value) {
|
||||
|
||||
const node = super.create();
|
||||
|
||||
node.setAttribute('alt', value.alt);
|
||||
node.setAttribute('src', value.url);
|
||||
|
||||
if (value.width) {
|
||||
node.setAttribute('width', value.width);
|
||||
}
|
||||
if (value.height) {
|
||||
node.setAttribute('height', value.height);
|
||||
}
|
||||
// 宽度和高度都不存在时,
|
||||
if (!value.width && !value.height) {
|
||||
node.setAttribute('display', 'block');
|
||||
node.setAttribute('width', '100%');
|
||||
}
|
||||
// 给图片添加点击事件
|
||||
node.onclick = () => {
|
||||
value.onClick && value.onClick(value.url);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static value (node) {
|
||||
|
||||
return {
|
||||
alt: node.getAttribute('alt'),
|
||||
url: node.getAttribute('src'),
|
||||
onclick: node.onclick,
|
||||
// width: node.width,
|
||||
// height: node.height,
|
||||
display: node.getAttribute('display')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ImageBlot.blotName = 'image';
|
||||
ImageBlot.tagName = 'img';
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:09:42
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-18 08:46:20
|
||||
*/
|
||||
import 'quill/dist/quill.core.css'; // 核心样式
|
||||
import 'quill/dist/quill.snow.css'; // 有工具栏
|
||||
import 'quill/dist/quill.bubble.css'; // 无工具栏
|
||||
import 'katex/dist/katex.min.css'; // katex 表达式样式
|
||||
import React, { useState, useReducer, useEffect } from 'react';
|
||||
import useQuill from './useQuill';
|
||||
|
||||
function ReactQuill ({
|
||||
disallowColors, // 不可见时颜色
|
||||
placeholder, // 提示信息
|
||||
uploadImage, // 图片上传
|
||||
onChange, // 内容变化时
|
||||
options, // 配置信息
|
||||
value, // 显示的内容
|
||||
style,
|
||||
showUploadImage // 显示上传图片
|
||||
}) {
|
||||
|
||||
const [element, setElement] = useState(); // quill 渲染节点
|
||||
|
||||
useQuill({
|
||||
disallowColors,
|
||||
placeholder,
|
||||
uploadImage,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
showUploadImage,
|
||||
element
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='react_quill_area' ref={setElement} style={style}/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactQuill;
|
@ -0,0 +1,47 @@
|
||||
function deepEqual (prev, current) {
|
||||
if (prev === current) { // 基本类型比较,值,类型都相同 或者同为 null or undefined
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!prev && current)
|
||||
|| (prev && !current)
|
||||
|| (!prev && !current)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(prev)) {
|
||||
if (!Array.isArray(current)) return false;
|
||||
if (prev.length !== current.length) return false;
|
||||
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (!deepEqual(current[i], prev[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof current === 'object') {
|
||||
if (typeof prev !== 'object') return false;
|
||||
const prevKeys = Object.keys(prev);
|
||||
const curKeys = Object.keys(current);
|
||||
|
||||
if (prevKeys.length !== curKeys.length) return false;
|
||||
|
||||
prevKeys.sort();
|
||||
curKeys.sort();
|
||||
|
||||
for (let i = 0; i < prevKeys.length; i++) {
|
||||
if (prevKeys[i] !== curKeys[i]) return false;
|
||||
const key = prevKeys[i];
|
||||
if (!deepEqual(prev[key], current[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default deepEqual;
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* @Description: 将多维数组转变成一维数组
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:35:01
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-16 11:36:22
|
||||
*/
|
||||
function flatten (array) {
|
||||
return flatten.rec(array, []);
|
||||
}
|
||||
|
||||
flatten.rec = function flatten (array, result) {
|
||||
|
||||
for (let item of array) {
|
||||
if (Array.isArray(item)) {
|
||||
flatten(item, result);
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default flatten;
|
@ -0,0 +1,32 @@
|
||||
#quill-toolbar{
|
||||
.quill-btn{
|
||||
vertical-align: middle;
|
||||
}
|
||||
.quill_image{
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
width: 28px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
.image_input{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.ql-image{
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react_quill_area{
|
||||
.ql-toolbar:not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
/*
|
||||
* @Description: 导出 ReactQuill
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:08:24
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-16 11:37:13
|
||||
*/
|
||||
import ReactQuill from './ReactQuill';
|
||||
import useQuill from './useQuill';
|
||||
|
||||
export default ReactQuill;
|
||||
export { useQuill };
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-12 19:48:55
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-16 11:38:16
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import deepEqual from './deepEqual';
|
||||
|
||||
function useDeepEqual (input) {
|
||||
|
||||
const [value, setValue] = useState(input);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!deepEqual(input, value)) {
|
||||
setValue(input)
|
||||
}
|
||||
|
||||
}, [input, value]);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default useDeepEqual;
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* @Description: 创建 reactQuill实例
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:31:42
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-17 20:42:05
|
||||
*/
|
||||
import Quill from 'quill'; // 导入quill
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import flatten from './flatten.js';
|
||||
import useDeepEqualMemo from './useDeepEqualMemo';
|
||||
import Katex from 'katex';
|
||||
import ImageBlot from './ImageBlot';
|
||||
import { fetchUploadImage } from '../../services/ojService.js';
|
||||
import { getImageUrl } from 'educoder'
|
||||
window.katex = Katex;
|
||||
|
||||
Quill.register(ImageBlot);
|
||||
|
||||
function useMountQuill ({
|
||||
element,
|
||||
options: passedOptions,
|
||||
uploadImage,
|
||||
showUploadImage,
|
||||
imgAttrs = {} // 指定图片的宽高属性
|
||||
}) {
|
||||
|
||||
// 是否引入 katex
|
||||
const [katexLoaded, setKatexLoaded] = useState(Boolean(window.katex))
|
||||
const [quill, setQuill] = useState(null);
|
||||
|
||||
const options = useDeepEqualMemo(passedOptions);
|
||||
console.log('use mount quill: ', passedOptions);
|
||||
|
||||
// 判断options中是否包含公式
|
||||
const requireKatex = useMemo(() => {
|
||||
return flatten(options.modules.toolbar).includes('formula');
|
||||
}, [options]);
|
||||
|
||||
// 加载katex
|
||||
useEffect(() => {
|
||||
if (!requireKatex) return;
|
||||
if (katexLoaded) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (window.katex) {
|
||||
setKatexLoaded(true);
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
|
||||
return () => { // 定义回调清除定时器
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
}, [
|
||||
setKatexLoaded,
|
||||
katexLoaded,
|
||||
requireKatex
|
||||
]);
|
||||
|
||||
// 加载 quill
|
||||
useEffect(() => {
|
||||
if (!element) return;
|
||||
if (requireKatex && !katexLoaded) {
|
||||
element.innerHTML = `
|
||||
<div style="color: #ddd">
|
||||
Loading Katex...
|
||||
</div>
|
||||
`
|
||||
}
|
||||
// 清空内容
|
||||
element.innerHTML = '';
|
||||
console.log(element);
|
||||
// 创建 quill 节点
|
||||
const quillNode = document.createElement('div');
|
||||
element.appendChild(quillNode); // 将quill节点追回到 element 元素中
|
||||
|
||||
const quill = new Quill(element, options);
|
||||
setQuill(quill);
|
||||
// 加载上传图片功能
|
||||
if (typeof uploadImage === 'function') {
|
||||
quill.getModule('toolbar').addHandler('image', (e) => {
|
||||
// 创建type类型输入框加载本地图片
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
input.click();
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = input.files[0]; // 获取文件信息
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// const reader = new FileReader();
|
||||
// reader.readAsDataURL(file);
|
||||
// console.log('文件信息===>>', reader);
|
||||
// reader.onload = function (e) {
|
||||
// debugger;
|
||||
// console.log('文件信息===>>', e.target.result);
|
||||
// const image = new Image();
|
||||
// image.src = e.target.result;
|
||||
|
||||
// image.onload = function () {
|
||||
// // file.width =
|
||||
// console.log(image.width, image.height);
|
||||
// }
|
||||
// }
|
||||
|
||||
const range = quill.getSelection(true);
|
||||
let fileUrl = ''; // 保存上传成功后图片的url
|
||||
// 上传文件
|
||||
const result = await fetchUploadImage(formData);
|
||||
// 获取上传图片的url
|
||||
if (result.data && result.data.id) {
|
||||
fileUrl = getImageUrl(`api/attachments/${result.data.id}`);
|
||||
}
|
||||
// 根据id获取文件路径
|
||||
const { width, height } = imgAttrs;
|
||||
// console.log('上传图片的url:', fileUrl);
|
||||
if (fileUrl) {
|
||||
quill.insertEmbed(range.index, 'image', {
|
||||
url: fileUrl,
|
||||
alt: '',
|
||||
onClick: showUploadImage,
|
||||
width,
|
||||
height
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
element.innerHTML = '';
|
||||
}
|
||||
}, [
|
||||
element,
|
||||
options,
|
||||
requireKatex,
|
||||
katexLoaded,
|
||||
]);
|
||||
|
||||
return quill;
|
||||
}
|
||||
|
||||
export default useMountQuill;
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:09:50
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-17 15:46:50
|
||||
*/
|
||||
import useQuillPlaceholder from './useQuillPlaceholder';
|
||||
import useQuillValueSync from './useQuillValueSync';
|
||||
import useQuillOnChange from './useQuillOnChange';
|
||||
import useMountQuill from './useMountQuill';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function useQuill ({
|
||||
disallowColors,
|
||||
placeholder,
|
||||
uploadImage,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
element,
|
||||
showUploadImage
|
||||
}) {
|
||||
|
||||
// 获取 quill 实例
|
||||
const quill = useMountQuill({
|
||||
element,
|
||||
options,
|
||||
uploadImage,
|
||||
showUploadImage
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (disallowColors && quill) {
|
||||
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
|
||||
delta.ops = delta.ops.map(op => {
|
||||
if (op.attributes && op.attributes.color) {
|
||||
const { color, ...attributes } = op.attributes;
|
||||
return {
|
||||
...op,
|
||||
attributes
|
||||
}
|
||||
}
|
||||
return op;
|
||||
});
|
||||
return delta;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
disallowColors,
|
||||
quill
|
||||
]);
|
||||
|
||||
useQuillPlaceholder(quill, placeholder);
|
||||
useQuillValueSync(quill, value);
|
||||
useQuillOnChange(quill, onChange);
|
||||
}
|
||||
|
||||
export default useQuill;
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-12 19:49:11
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-16 11:39:27
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function useQuillOnChange (quill, onChange) {
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!quill) return;
|
||||
if (typeof onChange !== 'function') return;
|
||||
|
||||
let handler;
|
||||
|
||||
quill.on(
|
||||
'text-change',
|
||||
(handler = () => {
|
||||
onChange(quill.getContents()); // getContents: 检索编辑器内容
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
quill.off('text-change', handler);
|
||||
}
|
||||
}, [quill, onChange]);
|
||||
}
|
||||
|
||||
export default useQuillOnChange;
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Author: tangjiang
|
||||
* @Github:
|
||||
* @Date: 2019-12-09 09:28:34
|
||||
* @LastEditors: tangjiang
|
||||
* @LastEditTime: 2019-12-16 11:39:48
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
|
||||
function useQuillPlaceholder (
|
||||
quill,
|
||||
placeholder
|
||||
) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!quill || !quill.root) return;
|
||||
quill.root.dataset.placeholder = placeholder;
|
||||
}, [quill, placeholder]);
|
||||
}
|
||||
|
||||
export default useQuillPlaceholder;
|
@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import deepEqual from './deepEqual.js'
|
||||
|
||||
function useQuillValueSync(quill, value) {
|
||||
const [selection, setSelection] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!quill) return
|
||||
|
||||
const previous = quill.getContents()
|
||||
const current = value
|
||||
|
||||
if (!deepEqual(previous, current)) {
|
||||
setSelection(quill.getSelection())
|
||||
if (typeof value === 'string') {
|
||||
quill.clipboard.dangerouslyPasteHTML(value, 'api')
|
||||
} else {
|
||||
quill.setContents(value)
|
||||
}
|
||||
}
|
||||
}, [quill, value, setSelection])
|
||||
|
||||
useEffect(() => {
|
||||
if (quill && selection) {
|
||||
quill.setSelection(selection)
|
||||
setSelection(null)
|
||||
}
|
||||
}, [quill, selection, setSelection])
|
||||
}
|
||||
|
||||
export default useQuillValueSync
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue