Commit 26df7420 by zangsuyun

Merge branch 'master' into feature/zangsuyun/20210315/knowledge

parents f8d9b028 1d242593
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
"underscore": "^1.10.2", "underscore": "^1.10.2",
"url-loader": "2.3.0", "url-loader": "2.3.0",
"video-react": "0.14.1", "video-react": "0.14.1",
"wangeditor": "^3.1.1", "wangeditor": "^4.6.9",
"webpack": "4.42.0", "webpack": "4.42.0",
"webpack-dev-server": "3.11.0", "webpack-dev-server": "3.11.0",
"webpack-manifest-plugin": "2.2.0", "webpack-manifest-plugin": "2.2.0",
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
} }
.iconfont { .iconfont {
color: #BFBFBF; color: #BFBFBF;
font-size:12px; font-size: 16px;
} }
} }
} }
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-03-19 16:45:42 * @Date: 2020-03-19 16:45:42
* @LastEditors: houyan * @LastEditors: yuananting
* @LastEditTime: 2020-06-24 11:24:54 * @LastEditTime: 2021-03-25 16:04:02
* @Description: * @Description:
*/ */
import XMEnum from './XMEnum'; import XMEnum from './XMEnum';
...@@ -29,12 +29,12 @@ export const RECORD_ERROR = new XMEnum([ ...@@ -29,12 +29,12 @@ export const RECORD_ERROR = new XMEnum([
{ {
name: 'NotAllowedError', name: 'NotAllowedError',
title: '请允许网页使用麦克风', title: '请允许网页使用麦克风',
content: '麦克风已被禁用,请现在浏览器设置中允许当前望重使用你的麦克风' content: '请检查开启麦克风权限后重试'
}, },
{ {
name: 'PermissionDeniedError', name: 'PermissionDeniedError',
title: '请允许浏览器使用麦克风', title: '请允许浏览器使用麦克风',
content: '麦克风已被禁用,请现在浏览器设置中允许使用麦克风' content: '请检查开启麦克风权限后重试'
}, },
{ {
name: 'NotFoundError', name: 'NotFoundError',
......
/* /*
* @Author: 陈剑宇 * @Author: 陈剑宇
* @Date: 2020-10-28 14:27:07 * @Date: 2020-10-28 14:27:07
* @LastEditTime: 2020-11-02 20:25:15 * @LastEditTime: 2021-03-18 10:30:41
* @LastEditors: 陈剑宇 * @LastEditors: yuananting
* @Description: * @Description:
* @FilePath: /xiaomai-web-b/app/common/constants/punchClock/punchClock.js * @FilePath: /xiaomai-web-b/app/common/constants/punchClock/punchClock.js
* @symbol_custom_string_obkoro1: Copyright © 2020 杭州杰竞科技有限公司 版权所有 * @symbol_custom_string_obkoro1: Copyright © 2020 杭州杰竞科技有限公司 版权所有
*/ */
import React from "react";
export const DEFAULT_IMG_URL = 'https://image.xiaomaiketang.com/xm/2MWCKBNiya.png'; export const DEFAULT_IMG_URL = 'https://image.xiaomaiketang.com/xm/2MWCKBNiya.png';
export const DEFAULT_CALENDAR_TEXT = '<p>亲爱的同学:</p><p>欢迎参加《麦麦教育0基础21天英语打卡活动》,大家都知道21天养成一个好习惯。在未来的21天里,老师将和同学们一起完成21个打卡任务,0基础锻炼口语发音能力,让听英语并且跟读英语成为生活中的一部分。</p><p>打卡时间:2020年6月1日-6月23日(共21天)</p><p>其中休息日:6月6日、6月7日</p><p>打卡任务:坚持21天英语打卡学习,并且分享至朋友圈</p><p>分享格式:麦麦教育0基础21天英语打卡+学生名字 +第X天 +坚持就是胜利!</p><p>打卡奖励:</p><p>①坚持完成“21天打卡”的宝贝可以获得精美小礼品!</p><p>②每天分享打卡任务到朋友圈的小宝贝,还可以获得一份大礼包哦~</p><p>老师有话说:小宝贝每天只可完成1个打卡任务,做到真正吃透后再进行下一个任务哦。</p>'; export const DEFAULT_CALENDAR_TEXT = '<p>亲爱的同学:</p><p>欢迎参加《麦麦教育0基础21天英语打卡活动》,大家都知道21天养成一个好习惯。在未来的21天里,老师将和同学们一起完成21个打卡任务,0基础锻炼口语发音能力,让听英语并且跟读英语成为生活中的一部分。</p><p>打卡时间:2020年6月1日-6月23日(共21天)</p><p>其中休息日:6月6日、6月7日</p><p>打卡任务:坚持21天英语打卡学习,并且分享至朋友圈</p><p>分享格式:麦麦教育0基础21天英语打卡+学生名字 +第X天 +坚持就是胜利!</p><p>打卡奖励:</p><p>①坚持完成“21天打卡”的宝贝可以获得精美小礼品!</p><p>②每天分享打卡任务到朋友圈的小宝贝,还可以获得一份大礼包哦~</p><p>老师有话说:小宝贝每天只可完成1个打卡任务,做到真正吃透后再进行下一个任务哦。</p>';
export const DEFAULT_PASS_TEXT = '<p>亲爱的同学:</p><p>欢迎参加《麦麦芭蕾形体初级课打卡活动》,我们将通过芭蕾的几个特性如开、绷、直等,使身体各部位发展均衡,宝贝们每天需学完当前课时并完成打卡,才能解锁下一个课时内容</p><p>打卡时间:2020年6月1日-6月21日(共21天)</p><p>关卡数:共15关</p><p>每日可解锁上限:2关</p><p>打卡任务:坚持初级课闯关打卡课程,并且分享至朋友圈</p><p>分享格式:麦麦芭蕾0基础形体课打卡+学生名字 +第X关 +坚持就是胜利!</p><p>打卡奖励:</p><p>①坚持完成“闯关打卡”的宝贝可以获得精美小礼品!</p><p>②每关都分享打卡任务到朋友圈的小宝贝,还可以获得一份大礼包哦~</p><p>老师有话说:小宝贝每天最多完成2个任务,做到真正吃透后再进行下一个任务哦</p>'; export const DEFAULT_PASS_TEXT = '<p>亲爱的同学:</p><p>欢迎参加《麦麦芭蕾形体初级课打卡活动》,我们将通过芭蕾的几个特性如开、绷、直等,使身体各部位发展均衡,宝贝们每天需学完当前课时并完成打卡,才能解锁下一个课时内容</p><p>打卡时间:2020年6月1日-6月21日(共21天)</p><p>关卡数:共15关</p><p>每日可解锁上限:2关</p><p>打卡任务:坚持初级课闯关打卡课程,并且分享至朋友圈</p><p>分享格式:麦麦芭蕾0基础形体课打卡+学生名字 +第X关 +坚持就是胜利!</p><p>打卡奖励:</p><p>①坚持完成“闯关打卡”的宝贝可以获得精美小礼品!</p><p>②每关都分享打卡任务到朋友圈的小宝贝,还可以获得一份大礼包哦~</p><p>老师有话说:小宝贝每天最多完成2个任务,做到真正吃透后再进行下一个任务哦</p>';
...@@ -112,6 +113,12 @@ export const FILE_ACCEPT = { ...@@ -112,6 +113,12 @@ export const FILE_ACCEPT = {
VOICE: 'audio/x-mpeg,audio/mp3,audio/mpeg,audio/wav,audio/x-m4a' VOICE: 'audio/x-mpeg,audio/mp3,audio/mpeg,audio/wav,audio/x-m4a'
} }
export const MEDIA_FILE_ACCEPT = {
PICTURE: 'image/jpg,image/jpeg,image/png,image/bmp,image/gif,JPG,JPEG,PNG,BMP,GIF',
VIDEO: 'audio/mp4,video/mp4,MP4',
VOICE: 'audio/x-mpeg,audio/mp3,audio/mpeg,audio/wav,audio/x-m4a,MP3'
}
export const QUESTION_FILE_ACCEPT = { export const QUESTION_FILE_ACCEPT = {
PICTURE: 'image/jpg,image/jpeg,image/png,image/gif', PICTURE: 'image/jpg,image/jpeg,image/png,image/gif',
VIDEO: 'audio/mp4,video/mp4', VIDEO: 'audio/mp4,video/mp4',
......
...@@ -146,6 +146,36 @@ class Upload { ...@@ -146,6 +146,36 @@ class Upload {
xhr.send(fd); xhr.send(fd);
}) })
} }
static uploadTextToOSS(string, name, success, error) {
if (!string) return success();
const params = {
accessTypeEnum: "PUBLIC",
bizCode: 'CLOUD_CLASS_COURSE',
instId: User.getStoreId(),
resourceName: name,
}
Service.Hades('/public/hades/ossAuthority', params).then((res) => {
const { resourceId, accessId, policy, callback, signature,key, host } = res.result;
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("OSSAccessKeyId", accessId);
formData.append("policy", policy);
formData.append("callback", callback);
formData.append("Signature", signature);
formData.append("key", key);
formData.append("file", new Blob([string]));
formData.append("success_action_status", 200);
xhr.open("POST", host);
xhr.onload = () => {
success(resourceId);
};
xhr.onerror = () => {
error();
}
xhr.send(formData);
})
}
} }
export default Upload; export default Upload;
\ No newline at end of file
/*
* @Author: yuananting
* @Date: 2021-03-03 15:13:12
* @LastEditors: yuananting
* @LastEditTime: 2021-03-17 11:40:41
* @Description: 助学工具接口
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import Service from "@/common/js/service";
export function queryCategoryTree(params: object) {
return Service.Hades("public/hades/queryCategoryTree", params);
}
export function addCategory(params: object) {
return Service.Hades("public/hades/addCategory", params);
}
export function delCategory(params: object) {
return Service.Hades("public/hades/delCategory", params);
}
export function editCategory(params: object) {
return Service.Hades("public/hades/editCategory", params);
}
export function editCategoryTree(params: object) {
return Service.Hades("public/hades/editCategoryTree", params);
}
export function queryQuestionCategoryTree(params: object) {
return Service.Hades("public/hades/queryQuestionCategoryTree", params);
}
export function queryQuestionPageList(params: object) {
return Service.Hades("public/hades/queryQuestionPageList", params);
}
export function addQuestion(params: object) {
return Service.Hades("public/hades/addQuestion", params);
}
export function deleteQuestion(params: object) {
return Service.Hades("public/hades/deleteQuestion", params);
}
export function queryQuestionDetails(params: object) {
return Service.Hades("public/hades/queryQuestionDetails", params);
}
export function editQuestion(params: object) {
return Service.Hades("public/hades/editQuestion", params);
}
export function batchImport(params: object) {
return Service.Hades("public/hades/batchImport", params);
}
\ No newline at end of file
/* /*
* @Author: 陈剑宇 * @Author: 陈剑宇
* @Date: 2020-05-07 14:43:01 * @Date: 2020-05-07 14:43:01
* @LastEditTime: 2021-03-22 13:54:27 * @LastEditTime: 2021-03-27 10:03:44
* @LastEditors: zangsuyun * @LastEditors: zangsuyun
* @Description: * @Description:
* @FilePath: /wheat-web-demo/src/domains/basic-domain/constants.ts * @FilePath: /wheat-web-demo/src/domains/basic-domain/constants.ts
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
import { MapInterface } from '@/domains/basic-domain/interface' import { MapInterface } from '@/domains/basic-domain/interface'
// 默认是 dev 环境 // 默认是 dev 环境
const ENV: string = process.env.DEPLOY_ENV || 'dev1'; const ENV: string = process.env.DEPLOY_ENV || 'dev';
console.log("process.env.DEPLOY_ENV",process) console.log("process.env.DEPLOY_ENV",process)
const BASIC_HOST_MAP: MapInterface = { const BASIC_HOST_MAP: MapInterface = {
dev: 'https://dev-heimdall.xiaomai5.com/', dev: 'https://dev-heimdall.xiaomai5.com/',
......
/*
* @Author: yuananting
* @Date: 2021-03-11 11:34:37
* @LastEditors: yuananting
* @LastEditTime: 2021-03-16 15:12:09
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import { queryCategoryTree, addCategory, delCategory, editCategory, editCategoryTree, queryQuestionCategoryTree, addQuestion, queryQuestionPageList, deleteQuestion, queryQuestionDetails, editQuestion, batchImport } from '@/data-source/questionBank/request-apis';
export default class QuestionBankService {
// 获取题目分类树
static queryCategoryTree(params: any) {
return queryCategoryTree(params);
}
// 新增题目分类
static addCategory(params: any) {
return addCategory(params);
}
// 删除分类
static delCategory(params: any) {
return delCategory(params);
}
// 编辑分类
static editCategory(params: any) {
return editCategory(params);
}
// 编辑分类树(拖拽)
static editCategoryTree(params: any) {
return editCategoryTree(params);
}
// 查询分类树列表
static queryQuestionCategoryTree(params: any) {
return queryQuestionCategoryTree(params);
}
// 查询题目列表
static queryQuestionPageList(params: any) {
return queryQuestionPageList(params);
}
// 添加题目
static addQuestion(params: any) {
return addQuestion(params);
}
// 删除题目
static deleteQuestion(params: any) {
return deleteQuestion(params);
}
// 预览题目
static queryQuestionDetails(params: any) {
return queryQuestionDetails(params);
}
// 编辑题目
static editQuestion(params: any) {
return editQuestion(params);
}
// 批量导入
static batchImport(params: any) {
return batchImport(params);
}
}
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-08-24 12:20:57 * @Date: 2020-08-24 12:20:57
* @LastEditors: zangsuyun * @LastEditors: zangsuyun
* @LastEditTime: 2021-03-25 12:21:52 * @LastEditTime: 2021-03-27 10:03:54
* @Description: * @Description:
* @Copyright: 杭州杰竞科技有限公司 版权所有 * @Copyright: 杭州杰竞科技有限公司 版权所有
--> -->
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-07-17 15:49:11 * @Date: 2020-07-17 15:49:11
* @Last Modified by: mikey.zhaopeng * @Last Modified by: chenshu
* @Last Modified time: 2020-11-23 22:02:49 * @Last Modified time: 2021-03-24 14:13:04
* @Description: 大班互动-添加/编辑直播课 * @Description: 大班互动-添加/编辑直播课
*/ */
...@@ -12,15 +12,20 @@ import { Button, message, Modal } from 'antd'; ...@@ -12,15 +12,20 @@ import { Button, message, Modal } from 'antd';
import ShowTips from "@/components/ShowTips"; import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs"; import Breadcrumbs from "@/components/Breadcrumbs";
import Bus from '../../core/bus'
import AddLiveBasic from './components/AddLiveBasic'; import AddLiveBasic from './components/AddLiveBasic';
import AddLiveClass from './components/AddLiveClass'; import AddLiveClass from './components/AddLiveClass';
import AddLiveIntro from './components/AddLiveIntro'; import AddLiveIntro from './components/AddLiveIntro';
import { randomString } from '@/domains/basic-domain/utils';
import Upload from '@/core/upload';
import PreviewCourseModal from './modal/PreviewCourseModal'; import PreviewCourseModal from './modal/PreviewCourseModal';
import SelectPrepareFileModal from '../prepare-lesson/modal/SelectPrepareFileModal';
import CourseService from "@/domains/course-domain/CourseService"; import CourseService from "@/domains/course-domain/CourseService";
import moment from 'moment'; import moment from 'moment';
import User from '@/common/js/user'; import User from '@/common/js/user';
import _ from "underscore"; import _ from "underscore";
import $ from 'jquery';
import './AddLive.less'; import './AddLive.less';
const defaultCover = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png'; const defaultCover = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
...@@ -97,12 +102,8 @@ class AddLive extends React.Component { ...@@ -97,12 +102,8 @@ class AddLive extends React.Component {
needRecord: 'YES', needRecord: 'YES',
whetherVisitorsJoin:'NO', whetherVisitorsJoin:'NO',
liveCourseWarmMedia: {}, liveCourseWarmMedia: {},
liveCourseMediaRequests: [{ introduce: '',
contentType:"INTRO", liveCourseMediaRequests: [],
mediaType: 'TEXT',
mediaContent: '',
key: Math.random()
}]
}, },
} }
} }
...@@ -112,6 +113,23 @@ class AddLive extends React.Component { ...@@ -112,6 +113,23 @@ class AddLive extends React.Component {
if (type === 'edit') { if (type === 'edit') {
this.getCourseDetail(); this.getCourseDetail();
} }
this.initBus();
}
componentWillUnmount() {
this.removeBus();
}
initBus = () => {
Bus.bind('graphicsEditorImage', this.uploadImage)
}
removeBus = () => {
Bus.unbind('graphicsEditorImage', this.uploadImage)
}
uploadImage = () => {
this.setState({ showSelectImageModal: true })
} }
getCourseDetail = () => { getCourseDetail = () => {
...@@ -140,6 +158,7 @@ class AddLive extends React.Component { ...@@ -140,6 +158,7 @@ class AddLive extends React.Component {
let coverUrl; let coverUrl;
let liveCourseMediaRequests = []; let liveCourseMediaRequests = [];
let liveCourseWarmMedia; let liveCourseWarmMedia;
let hasIntro = false;
courseMediaVOS.map((item) => { courseMediaVOS.map((item) => {
switch (item.contentType){ switch (item.contentType){
case "COVER": case "COVER":
...@@ -150,7 +169,8 @@ class AddLive extends React.Component { ...@@ -150,7 +169,8 @@ class AddLive extends React.Component {
liveCourseWarmMedia = item; liveCourseWarmMedia = item;
break; break;
case "INTRO": case "INTRO":
liveCourseMediaRequests = [...liveCourseMediaRequests,item] hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break; break;
default: default:
break; break;
...@@ -200,6 +220,7 @@ class AddLive extends React.Component { ...@@ -200,6 +220,7 @@ class AddLive extends React.Component {
isEdit = false isEdit = false
} }
this.setState({ this.setState({
loadintroduce: !hasIntro,
isEdit, isEdit,
loading: false, loading: false,
courseState, courseState,
...@@ -210,6 +231,21 @@ class AddLive extends React.Component { ...@@ -210,6 +231,21 @@ class AddLive extends React.Component {
}) })
} }
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ addLiveIntroInfo: { ...this.state.addLiveIntroInfo, [key]: res }, [`load${key}`]: true });
},
error: () => {
message.warning('获取简介失败')
}
})
}
// 修改基本信息 // 修改基本信息
// 修改基本信息 // 修改基本信息
handleChangeBasicInfo = (field, value) => { handleChangeBasicInfo = (field, value) => {
...@@ -274,6 +310,13 @@ handleChangeBasicInfo = (field, value) => { ...@@ -274,6 +310,13 @@ handleChangeBasicInfo = (field, value) => {
} }
this.handleValidate(addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit).then((res) => { this.handleValidate(addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit).then((res) => {
if (!res) return; if (!res) return;
Upload.uploadTextToOSS(addLiveIntroInfo.introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({ introduceId, addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id });
}, () => message.warning('上传课程简介失败'));
})
}
submitRemote = ({ introduceId, addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, id }) => {
const { type } = this.state; const { type } = this.state;
const { courseName,coverUrl,coverId,categoryId} = addLiveBasicInfo; const { courseName,coverUrl,coverId,categoryId} = addLiveBasicInfo;
const { const {
...@@ -286,7 +329,7 @@ handleChangeBasicInfo = (field, value) => { ...@@ -286,7 +329,7 @@ handleChangeBasicInfo = (field, value) => {
} = addLiveClassInfo; } = addLiveClassInfo;
let { startTime, endTime } = addLiveClassInfo; let { startTime, endTime } = addLiveClassInfo;
const { liveCourseMediaRequests, needRecord,whetherVisitorsJoin,liveCourseWarmMedia} = addLiveIntroInfo; const { needRecord,whetherVisitorsJoin,liveCourseWarmMedia} = addLiveIntroInfo;
if(type === 'add') { if(type === 'add') {
startTime = startTime; startTime = startTime;
...@@ -305,7 +348,7 @@ handleChangeBasicInfo = (field, value) => { ...@@ -305,7 +348,7 @@ handleChangeBasicInfo = (field, value) => {
mediaType:'PICTURE', mediaType:'PICTURE',
mediaUrl: coverUrl, mediaUrl: coverUrl,
} }
let scheduleMediaRequests = [...liveCourseMediaRequests]; let scheduleMediaRequests = [];
if(coverId){ if(coverId){
scheduleMediaRequests = [coverObj,...scheduleMediaRequests] scheduleMediaRequests = [coverObj,...scheduleMediaRequests]
} }
...@@ -329,7 +372,8 @@ handleChangeBasicInfo = (field, value) => { ...@@ -329,7 +372,8 @@ handleChangeBasicInfo = (field, value) => {
if (type === 'add') { if (type === 'add') {
const params = { const params = {
...commonParams, ...commonParams,
operatorId:User.getUserId(), operatorId: User.getUserId(),
introduceId,
} }
CourseService.createLiveCloudCourse(params).then((res) => { CourseService.createLiveCloudCourse(params).then((res) => {
...@@ -346,6 +390,7 @@ handleChangeBasicInfo = (field, value) => { ...@@ -346,6 +390,7 @@ handleChangeBasicInfo = (field, value) => {
...commonParams, ...commonParams,
updateUserId:User.getUserId(), updateUserId:User.getUserId(),
liveCourseId: id, liveCourseId: id,
introduceId,
} }
CourseService.updateLiveCloudCourse(params).then((res) => { CourseService.updateLiveCloudCourse(params).then((res) => {
if (res.success){ if (res.success){
...@@ -356,8 +401,8 @@ handleChangeBasicInfo = (field, value) => { ...@@ -356,8 +401,8 @@ handleChangeBasicInfo = (field, value) => {
} }
}); });
} }
})
} }
handleValidate = (addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit) => { handleValidate = (addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, isEdit) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const { type } = this.state; const { type } = this.state;
...@@ -498,14 +543,33 @@ handleChangeBasicInfo = (field, value) => { ...@@ -498,14 +543,33 @@ handleChangeBasicInfo = (field, value) => {
} }
} }
handleSelectImage = (file) => {
this.setState({
showSelectImageModal: false
})
const { ossUrl } = file;
const { addLiveIntroInfo } = this.state;
this.setState({
addLiveIntroInfo: {
...addLiveIntroInfo,
introduce: `${addLiveIntroInfo.introduce}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`
}
});
}
render() { render() {
const { const {
id, type, id,
addLiveBasicInfo, addLiveClassInfo, addLiveIntroInfo, type,
isEdit addLiveBasicInfo,
addLiveClassInfo,
addLiveIntroInfo,
isEdit,
loadintroduce,
showSelectImageModal,
} = this.state; } = this.state;
console.log(loadintroduce, addLiveIntroInfo, 888888)
return ( return (
<div className="page add-live-page"> <div className="page add-live-page">
<Breadcrumbs <Breadcrumbs
...@@ -541,7 +605,7 @@ handleChangeBasicInfo = (field, value) => { ...@@ -541,7 +605,7 @@ handleChangeBasicInfo = (field, value) => {
<div className="title">更多信息</div> <div className="title">更多信息</div>
<AddLiveIntro <AddLiveIntro
isEdit={isEdit} isEdit={isEdit}
data={addLiveIntroInfo} data={{ ...addLiveIntroInfo, loadintroduce, id }}
onChange={this.handleChangeIntroInfo} onChange={this.handleChangeIntroInfo}
/> />
</div> </div>
...@@ -550,10 +614,25 @@ handleChangeBasicInfo = (field, value) => { ...@@ -550,10 +614,25 @@ handleChangeBasicInfo = (field, value) => {
<div className="footer"> <div className="footer">
<Button onClick={this.handleGoBack}>取消</Button> <Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览课程介绍</Button> <Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>保存</Button> <Button type="primary" onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>保存</Button>
</div> </div>
{showSelectImageModal &&
<SelectPrepareFileModal
key="basic"
operateType="select"
multiple={false}
accept="image/jpeg,image/png,image/jpg"
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectImageModal}
onClose={() => {
this.setState({ showSelectImageModal: false })
}}
onSelect={this.handleSelectImage}
/>
}
{ this.state.previewLiveCourseModal } { this.state.previewLiveCourseModal }
{ this.state.lackConsumeStudentModal } { this.state.lackConsumeStudentModal }
</div> </div>
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-07-16 11:05:17 * @Date: 2020-07-16 11:05:17
* @Last Modified by: mikey.zhaopeng * @Last Modified by: chenshu
* @Last Modified time: 2020-11-24 14:29:52 * @Last Modified time: 2021-03-23 17:39:50
* @Description: 添加直播-简介 * @Description: 添加直播-简介
*/ */
import React from 'react'; import React from 'react';
import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd'; import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd';
import Service from '@/common/js/service'; import Service from '@/common/js/service';
import EditorBox from '../components/EditorBox'; import GraphicsEditor from './GraphicsEditor';
import User from '@/common/js/user'; import User from '@/common/js/user';
import UploadOss from '@/core/upload'; import UploadOss from '@/core/upload';
import './AddLiveIntro.less'; import './AddLiveIntro.less';
...@@ -138,32 +138,10 @@ class AddLiveIntro extends React.Component { ...@@ -138,32 +138,10 @@ class AddLiveIntro extends React.Component {
) )
} }
handleChangeIntro = (index, value, length) => { changeIntro = (value) => {
const { liveCourseMediaRequests } = this.props.data; this.props.onChange('introduce', value);
console.log('index',index);
liveCourseMediaRequests[index].mediaContent = value;
liveCourseMediaRequests[index].mediaContentLength = length
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
handleAddIntroText = () => {
const { liveCourseMediaRequests } = this.props.data;
liveCourseMediaRequests.push({
contentType:"INTRO",
mediaType: 'TEXT',
mediaContent: '',
key: Math.random()
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
} }
handleUpload = (Blob) => {
this.setState({
showSelectFileModal: true,
selectType:'INTRO'
})
}
whetherVisitorsJoinChange = ()=>{ whetherVisitorsJoinChange = ()=>{
if(this.props.data.whetherVisitorsJoin==="NO"){ if(this.props.data.whetherVisitorsJoin==="NO"){
this.props.onChange('whetherVisitorsJoin','YES') this.props.onChange('whetherVisitorsJoin','YES')
...@@ -176,9 +154,8 @@ class AddLiveIntro extends React.Component { ...@@ -176,9 +154,8 @@ class AddLiveIntro extends React.Component {
} }
render() { render() {
const {liveType, isXiaomai, isEdit, data: { introduction, needRecord,whetherVisitorsJoin,liveCourseMediaRequests = [], liveCourseWarmMedia = {} } } = this.props; const {liveType, isXiaomai, isEdit, data: { id, introduce, needRecord,whetherVisitorsJoin, loadintroduce, liveCourseWarmMedia = {} } } = this.props;
const { showCutModal, warmUrl, showSelectFileModal, diskList, imageFile,selectType} = this.state const { showCutModal, warmUrl, showSelectFileModal, diskList, imageFile,selectType} = this.state
console.log('liveCourseMediaRequests',liveCourseMediaRequests);
return ( return (
<div className="add-live__intro-info"> <div className="add-live__intro-info">
<div className="playback" > <div className="playback" >
...@@ -195,7 +172,7 @@ class AddLiveIntro extends React.Component { ...@@ -195,7 +172,7 @@ class AddLiveIntro extends React.Component {
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col span={8}> <Col span={24}>
<Radio value="NO"> <Radio value="NO">
手动录制 手动录制
<span className="playback__text">讲师手动选择何时开始录制</span> <span className="playback__text">讲师手动选择何时开始录制</span>
...@@ -267,49 +244,19 @@ class AddLiveIntro extends React.Component { ...@@ -267,49 +244,19 @@ class AddLiveIntro extends React.Component {
<span className="label">直播课简介:</span> <span className="label">直播课简介:</span>
<div className="content"> <div className="content">
<div className="intro-list"> <div className="intro-list">
{ <div className="intro-list__item introduce-editor">
liveCourseMediaRequests.map((item, index) => { {(!id || loadintroduce) &&
if (item.mediaType === 'TEXT') { <GraphicsEditor
return ( id="intro"
<div className="intro-list__item" key={item.key}> isIntro={true}
<EditorBox
detail={{ detail={{
content: item.mediaContent content: introduce
}} }}
onChange={(val, length) => { this.handleChangeIntro(index, val, length) }} onChange={(val) => { this.changeIntro(val) }}
/> />
{this.renderLittleIcon(index)}
</div>
)
} }
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-list__item picture" key={index}>
<div className="img__wrap">
<img src={item.mediaUrl} />
</div> </div>
{this.renderLittleIcon(index)}
</div> </div>
)
}
})
}
</div>
<div className="operate">
<div className="operate__item" onClick={this.handleAddIntroText}>
<span className="icon iconfont">&#xe639;</span>
<span className="text">文字</span>
</div>
<div className="operate__item" onClick={this.handleUpload}>
<span className="icon iconfont">&#xe63b;</span>
<span className="text">图片</span>
</div>
</div>
<div className="tips">
• 图片支持jpeg、jpg、png、gif格式
</div>
</div> </div>
</div> </div>
{/* 选择暖场图文件弹窗 */} {/* 选择暖场图文件弹窗 */}
......
...@@ -21,7 +21,7 @@ class EditorBox extends React.Component { ...@@ -21,7 +21,7 @@ class EditorBox extends React.Component {
const { editorId } = this.state; const { editorId } = this.state;
const { detail, onChange } = this.props; const { detail, onChange } = this.props;
const editorInt = new E(`#editor${editorId}`); const editorInt = new E(`#editor${editorId}`);
editorInt.customConfig.menus = [ editorInt.config.menus = [
// 'head', // 标题 // 'head', // 标题
'bold', // 粗体 'bold', // 粗体
// 'fontSize', // 字号 // 'fontSize', // 字号
...@@ -36,18 +36,18 @@ class EditorBox extends React.Component { ...@@ -36,18 +36,18 @@ class EditorBox extends React.Component {
'emoticon', // 表情 'emoticon', // 表情
] ]
editorInt.customConfig.emotions = [ editorInt.config.emotions = [
{ {
title: 'emoji', title: 'emoji',
type: 'emoji', type: 'emoji',
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐'] content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐']
} }
] ]
editorInt.customConfig.zIndex = 1; editorInt.config.zIndex = 1;
editorInt.customConfig.pasteFilterStyle = false; editorInt.config.pasteFilterStyle = false;
editorInt.customConfig.pasteIgnoreImg = true; editorInt.config.pasteIgnoreImg = true;
// 自定义处理粘贴的文本内容 // 自定义处理粘贴的文本内容
editorInt.customConfig.pasteTextHandle = function (content) { editorInt.config.pasteTextHandle = function (content) {
if (content == '' && !content) return '' if (content == '' && !content) return ''
var str = content var str = content
str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '') str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '')
...@@ -56,7 +56,7 @@ class EditorBox extends React.Component { ...@@ -56,7 +56,7 @@ class EditorBox extends React.Component {
str = str.replace(/\&nbsp\;/ig, ' ') str = str.replace(/\&nbsp\;/ig, ' ')
return str return str
} }
editorInt.customConfig.onchange = (html) => { editorInt.config.onchange = (html) => {
const textLength = editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length; const textLength = editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length;
this.setState({ textLength }, () => { this.setState({ textLength }, () => {
onChange(html, this.state.textLength); onChange(html, this.state.textLength);
...@@ -64,7 +64,6 @@ class EditorBox extends React.Component { ...@@ -64,7 +64,6 @@ class EditorBox extends React.Component {
} }
editorInt.create(); editorInt.create();
editorInt.txt.html(detail.content); editorInt.txt.html(detail.content);
editorInt.change && editorInt.change();
} }
render() { render() {
......
import React from 'react';
import E from 'wangeditor';
import Bus from '../../../core/bus';
import './GraphicsEditor.less';
const { BtnMenu } = E;
class ImageMenu extends BtnMenu {
constructor(editor) {
// data-title属性表示当鼠标悬停在该按钮上时提示该按钮的功能简述
const $elem = E.$(
`<div class="w-e-menu" data-title="图片">
<i class="w-e-icon-image"></i>
</div>`
)
super($elem, editor)
}
// 菜单点击事件
clickHandler() {
Bus.trigger('graphicsEditorImage')
}
tryChangeActive() {
}
}
class VideoMenu extends BtnMenu {
constructor(editor) {
// data-title属性表示当鼠标悬停在该按钮上时提示该按钮的功能简述
const $elem = E.$(
`<div class="w-e-menu" data-title="视频">
<i class="w-e-icon-play"></i>
</div>`
)
super($elem, editor)
}
// 菜单点击事件
clickHandler() {
Bus.trigger('graphicsEditorVideo')
}
tryChangeActive() {
}
}
class GraphicsEditor extends React.Component {
constructor(props) {
super(props)
this.state = {
editorId: window.random_string(16),
textLength: 0,
}
this.editorInt = null;
}
componentDidMount() {
this.renderEditor()
this.resetIndex(true);
}
componentWillReceiveProps(nextProps) {
const { content } = this.props.detail;
const { content: nextContent } = nextProps.detail;
const videoCount = ((content || '').match(/<iframe/g) || []).length;
const nextVideoCount = ((nextContent || '').match(/<iframe/g) || []).length;
const imageCount = ((content || '').match(/<img/g) || []).length;
const nextImageCount = ((nextContent || '').match(/<img/g) || []).length;
if ((videoCount !== nextVideoCount) || (imageCount !== nextImageCount)) {
this.editorInt && this.editorInt.txt.html(nextProps.detail.content);
}
}
componentWillUnmount() {
this.resetIndex();
}
resetIndex = (bool) => {
const topDom = document.querySelector('.top-container');
const leftDom = document.querySelector('.left-container');
topDom.style.zIndex = bool ? 'auto' : 112;
leftDom.style.zIndex = bool ? 'auto' : 2;
}
renderEditor() {
const { editorId } = this.state;
const { detail, onChange, isIntro } = this.props;
this.editorInt = new E(`#editor${editorId}`);
this.editorInt.config.showFullScreen = !isIntro
this.editorInt.menus.extend('xmimage', ImageMenu);
this.editorInt.menus.extend('xmvideo', VideoMenu);
this.editorInt.config.menus = isIntro ?
[
'head',
'bold',
'fontSize',
'fontName',
'italic',
'underline',
'strikeThrough',
'foreColor',
'backColor',
'list',
'justify',
'emoticon',
'xmimage',
]
: [
'head',
'bold',
'fontSize',
'fontName',
'italic',
'underline',
'strikeThrough',
'indent',
'lineHeight',
'foreColor',
'backColor',
'link',
'list',
'todo',
'justify',
'quote',
'emoticon',
'xmimage',
'xmvideo',
'table',
'splitLine',
'undo',
'redo',
];
this.editorInt.config.emotions = [
{
title: 'emoji',
type: 'emoji',
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐']
}
]
this.editorInt.config.zIndex = 1;
this.editorInt.config.pasteFilterStyle = false;
this.editorInt.config.pasteIgnoreImg = true;
// 自定义处理粘贴的文本内容
this.editorInt.config.pasteTextHandle = function (content) {
if (content == '' && !content) return ''
var str = content
str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '')
str = str.replace(/<style>[\s\S]*?<\/style>/ig, '')
str = str.replace(/[ | ]*\n/g, '\n')
str = str.replace(/\&nbsp\;/ig, ' ')
return str
}
this.editorInt.config.onchange = (html) => {
const videoCount = ((html || '').match(/<iframe/g) || []).length;
const imageCount = ((html || '').match(/<img/g) || []).length;
const textLength = this.editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length + videoCount + imageCount;
console.log(html, 777777)
this.setState({ textLength }, () => {
onChange(html, this.state.textLength);
})
}
this.editorInt.create();
this.editorInt.txt.html(detail.content);
}
render() {
const { editorId, textLength } = this.state;
const { limitLength = 1000, isIntro } = this.props;
return <div className={`graphics-editor-container${isIntro ? ' introduce' : ''}`}>
<div className="editor-box" id={`editor${editorId}`}></div>
<div className="editor-tips">({textLength}/100000)</div>
</div>
}
}
export default GraphicsEditor;
.graphics-editor-container {
border: 1px solid #E8E8E8;
border-radius: 4px;
width: 702px;
height: 510px;
position: relative;
.w-e-text p,
.w-e-text h1,
.w-e-text h2,
.w-e-text h3,
.w-e-text h4,
.w-e-text h5,
.w-e-text table,
.w-e-text pre {
margin: 0;
}
.editor-box {
height: 100%;
}
.w-e-toolbar {
background-color: #fff !important;
border: none !important;
border-bottom: 1px solid #E8E8E8 !important;
width: 700px;
}
.w-e-text-container {
border: none !important;
height: ~'calc(100% - 109px)' !important;
}
.editor-tips {
position: absolute;
bottom: 5px;
right: 8px;
color: #666;
z-index: 1;
}
.w-e-full-screen-editor {
.w-e-text-container {
height: ~'calc(100vh - 109px)' !important;
}
}
&.introduce {
height: 200px;
.w-e-text-container {
height: ~'calc(100% - 69px)' !important;
}
}
}
\ No newline at end of file
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-07-14 15:43:00 * @Date: 2020-07-14 15:43:00
* @Last Modified by: mikey.zhaopeng * @Last Modified by: chenshu
* @Last Modified time: 2020-11-23 20:23:12 * @Last Modified time: 2021-03-16 17:37:23
* @Description: 大班直播、互动班课的直播课列表 * @Description: 大班直播、互动班课的直播课列表
*/ */
...@@ -83,6 +83,7 @@ class LiveCourseList extends React.Component { ...@@ -83,6 +83,7 @@ class LiveCourseList extends React.Component {
needStr={needStr} needStr={needStr}
data={shareData} data={shareData}
type="liveClass" type="liveClass"
title="直播课"
close={() => { close={() => {
this.setState({ this.setState({
shareLiveModal: null shareLiveModal: null
......
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-04 10:26:07
* @Description: 图文课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Button, Input, Radio, message, Modal,Cascader} from 'antd';
import $ from 'jquery';
import { DISK_MAP, FileTypeIcon, FileVerifyMap } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs";
import Bus from '../../../core/bus'
import AddGraphicsIntro from './components/AddGraphicsIntro';
import SelectStudent from '../modal/select-student';
import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFileModal';
import PreviewGraphicsModal from '../modal/PreviewGraphicsModal';
import StoreService from "@/domains/store-domain/storeService";
import Service from '@/common/js/service';
import { randomString } from '@/domains/basic-domain/utils';
import User from '@/common/js/user';
import _ from "underscore";
import Upload from '@/core/upload';
import './AddGraphicsCourse.less';
const EDIT_BOX_KEY = Math.random();
const fieldNames = { label: 'categoryName', value: 'id', children: 'sonCategoryList' };
//添加课程时课程默认的一些值
const defaultShelfState = 'YES';
const whetherVisitorsJoin = 'NO'
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
let cutFlag = false;
class AddGraphicsCourse extends React.Component {
constructor(props) {
super(props);
const id = getParameterByName("id");
const pageType = getParameterByName("type");
this.state = {
id, // 图文课ID,编辑的时候从URL上带过来
pageType, // 页面类型: add->新建 edit->编辑
imageFile: null, // 需要被截取的图片
courseName: null, // 图文课名称
courseMedia: '',
introduce: '',
courseMediaId: null, // 图文课链接
coverId: null, // 图文封面的recourceId
coverUrl: defaultCoverUrl, // 图文课封面
studentList: [], // 上课学员列表
shelfState:'YES', //是否开启店铺展示
diskList: [], // 机构可见磁盘目录
selectedFileList: [], // 已经从资料云盘中勾选的文件
showCutModal: false, // 是否显示截图弹窗
showSelectVideoModal: false,
studentModal: false,
categoryName:null, //分类名称
courseCatalogList:[], //分类列表
categoryId:null, //分类的Id值
whetherVisitorsJoin: 'NO', // 是否允许游客加入
isContent: true,
}
}
componentDidMount() {
this.initBus()
}
componentWillMount() {
const { id, pageType } = this.state;
this.getCourseCatalogList();
if (pageType === 'edit') {
this.handleFetchScheudleDetail(id);
}
}
componentWillUnmount() {
this.removeBus();
}
initBus = () => {
Bus.bind('graphicsEditorImage', this.uploadImage)
Bus.bind('graphicsEditorVideo', this.uploadVideo)
}
removeBus = () => {
Bus.unbind('graphicsEditorImage', this.uploadImage)
Bus.unbind('graphicsEditorVideo', this.uploadVideo)
}
uploadImage = () => {
this.setState({ showSelectImageModal: true })
}
uploadVideo = () => {
this.setState({ showSelectVideoModal: true })
}
//获取分类列表
getCourseCatalogList = ()=>{
StoreService.getCourseCatalogList({current:1,size:1000}).then((res) => {
this.setState({
courseCatalogList:res.result.records
})
});
}
catalogChange= (value, options) => {
const changeValueLength = value.length;
switch (changeValueLength){
case 1:
this.setState({ categoryId: value[0], categoryName: options[0].categoryName });
break;
case 2:
this.setState({ categoryId: value[1], categoryName: `${options[0].categoryName}-${options[1].categoryName}` });
break;
default:
this.setState({ categoryId: null, categoryName: '' });
break;
}
}
// 获取图文课详情
handleFetchScheudleDetail = (courseId) => {
Service.Hades('public/hades/mediaCourseDetail',{
courseId
}).then((res) => {
const { result = {} } = res || {};
const {
courseName,
shelfState,
whetherVisitorsJoin,
courseMediaVOS,
categoryOneName,
categoryTwoName,
categoryId
} = result;
let coverId;
let coverUrl = this.state.coverUrl;
let hasIntro = false;
courseMediaVOS.map((item) => {
switch (item.contentType){
case "COVER":
coverId = item.mediaContent;
coverUrl = item.mediaUrl;
break;
case "SCHEDULE":
this.getTextDetail('courseMedia', item.mediaUrl);
break;
case "INTRO":
hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break;
default:
break;
}
return item;
})
let categoryName;
if( categoryTwoName ){
categoryName = `${categoryOneName}-${categoryTwoName}`;
}else{
categoryName = `${categoryOneName}`;
}
this.setState({
loadintroduce: !hasIntro,
coverId,
coverUrl,
courseName,
shelfState,
whetherVisitorsJoin,
categoryName,
categoryId
});
})
}
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ [key]: res, [`load${key}`]: true });
}
})
}
handleGoBack = () => {
const {
coverId,
videoName,
videoDuration,
courseName,
courseMediaId,
categoryId,
shelfState,
whetherVisitorsJoin
} = this.state;
if(videoName || videoDuration || courseMediaId || categoryId || courseName || coverId || shelfState !== defaultShelfState || whetherVisitorsJoin !== whetherVisitorsJoin ){
Modal.confirm({
title: '确认要返回吗?',
content: '返回后,本次编辑的内容将不被保存。',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
onOk: () => {
RCHistory.goBack();
}
});
}else{
RCHistory.goBack();
}
}
// 修改表单
handleChangeForm = (field, value, coverUrl) => {
this.setState({
[field]: value,
coverUrl: coverUrl ? coverUrl : this.state.coverUrl
});
}
// 显示选择学员弹窗
handleShowSelectStuModal = () => {
this.setState({ studentModal : true });
const { studentList, selectedStuList } = this.state;
// const _studentList = _.map(studentList, (item) => {
// return item.studentId
// })
const studentModal = (
<SelectStudent
showTabs={true}
type="videoCourse"
onSelect={this.handleSelectStudent}
after={true} //表明是不是上课后的状态
studentList={studentList}
close={() => {
this.setState({
studentModal: null,
});
}}
/>
)
this.setState({ studentModal });
}
handleSelectStudent = (studentIds) => {
let studentList = [];
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
// this.setState({ studentModal: null });
this.setState({ studentList });
this.setState({ studentModal : false });
}
// 显示预览弹窗
handleShowPreviewModal = () => {
const {
coverUrl,
courseName,
courseMedia,
introduce,
categoryName,
} = this.state;
const courseBasinInfo = {
coverUrl,
courseName,
categoryName
}
const courseIntroInfo = {
courseMedia,
introduce,
}
const previewGraphicsModal = (
<PreviewGraphicsModal
courseBasicInfo={courseBasinInfo}
courseIntroInfo={courseIntroInfo}
close={() => {
this.setState({
previewGraphicsModal: null
})
}}
/>
);
this.setState({ previewGraphicsModal });
}
// 选择图文
handleSelectVideo = (file) => {
this.setState({
showSelectVideoModal: false
})
const { ossUrl } = file;
const { courseMedia } = this.state;
this.setState({
courseMedia: `${courseMedia}<p style="width: 100%;padding-top: 56.25%;position: relative;"><iframe style="position: absolute;width: 100%;height: 100%;top: 0;left: 0;" src="${ossUrl}"></iframe><br/></p><p><br/></p>`
});
}
handleSelectImage = (file) => {
this.setState({
showSelectImageModal: false
})
const { ossUrl } = file;
const { courseMedia, introduce, isContent } = this.state;
this.setState({
[isContent ? 'courseMedia' : 'introduce']: `${isContent ? courseMedia : introduce}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`
});
}
handleSelectCover = (file)=> {
this.uploadCoverImage(file);
}
//上传图片
uploadCoverImage = (imageFile) => {
debugger
const { folderName } = imageFile;
const fileName = window.random_string(16) + folderName.slice(folderName.lastIndexOf("."));
const self = this;
this.setState(
{
visible: true,
},
() => {
setTimeout(() => {
const okBtnDom = document.querySelector("#headPicModal");
const options = {
size: [500, 282],
ok: okBtnDom,
maxZoom: 3,
style: {
jpgFillColor: "transparent",
},
done: function (dataUrl) {
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if ((self.state.rotate != this.rotate()) || (self.state.scale != this.scale())) {
const _dataUrl = this.clip()
const cutImageBlob = self.convertBase64UrlToBlob(_dataUrl);
self.setState({
cutImageBlob,
dataUrl: _dataUrl,
rotate: this.rotate(),
scale: this.scale()
})
}
}, 500)
const cutImageBlob = self.convertBase64UrlToBlob(dataUrl);
self.setState({
cutImageBlob,
dataUrl
})
setTimeout(() => {
cutFlag = false;
}, 2000);
},
fail: (failInfo) => {
message.error("图片上传失败了,请重新上传");
},
loadComplete: function (img) {
setTimeout(() => {
const _dataUrl = this.clip()
self.setState({
dataUrl: _dataUrl,
hasImgReady: true
})
}, 100)
},
};
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`
if (!this.state.photoclip) {
const _photoclip = new PhotoClip("#headPicModal", options);
_photoclip.load(imgUrl);
this.setState({
photoclip: _photoclip,
});
} else {
this.state.photoclip.clear();
this.state.photoclip.load(imgUrl);
}
}, 200);
}
);
};
//获取resourceId
getSignature = (blob, fileName) => {
Upload.uploadBlobToOSS(blob, 'cover' + (new Date()).valueOf(),null,'signInfo').then((signInfo) => {
this.setState({
coverClicpPath:signInfo.fileUrl,
coverId:signInfo.resourceId,
visible: false
},()=>this.updateCover())
});
};
updateCover = () =>{
const {coverClicpPath,coverId} = this.state
this.setState({
showSelectCoverModal: false,
coverUrl:coverClicpPath,
coverId:coverId
})
}
// base64转换成blob
convertBase64UrlToBlob = (urlData) => {
const bytes = window.atob(urlData.split(",")[1]);
const ab = new ArrayBuffer(bytes.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: "image/png" });
};
// 保存
handleSubmit = () => {
const {
id,
coverId,
pageType,
courseName,
courseMedia,
introduce,
categoryId,
shelfState,
whetherVisitorsJoin,
} = this.state;
const commonParams = {
categoryId,
courseName,
coverId,
operatorId:User.getStoreUserId(),
storeId:User.getStoreId(),
shelfState,
whetherVisitorsJoin,
courseType: 'PICTURE',
};
// 校验必填字段:课程名称, 课程图文
this.handleValidate(courseName, courseMedia, categoryId).then((res) => {
if (!res) return;
Upload.uploadTextToOSS(courseMedia, `${randomString()}.txt`, (courseMediaId) => {
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({
id,
pageType,
commonParams,
courseMediaId,
introduceId,
});
}, () => message.warning('上传课程简介失败'));
}, () => message.warning('上传课程内容失败'));
});
}
submitRemote = (data) => {
const { id, pageType, commonParams, courseMediaId, introduceId } = data;
commonParams.courseMediaId = courseMediaId;
commonParams.introduceId = introduceId;
if (pageType === 'add') {
Service.Hades('public/hades/createMediaCourse', commonParams).then((res) => {
if (!res) return;
message.success("新建成功");
window.RCHistory.goBack();
})
} else {
const editParams = {
courseId:id,
...commonParams,
}
Service.Hades('public/hades/editMediaCourse', editParams).then((res) => {
if (!res) return;
message.success("保存成功");
window.RCHistory.goBack();
});
}
}
handleValidate = (courseName, courseMedia, categoryId) => {
return new Promise((resolve) => {
if (!courseName) {
message.warning('请输入课程名称');
resolve(false);
return false
}
if (!courseMedia) {
message.warning('请输入课程内容');
resolve(false);
return false
}
if(!categoryId){
message.warning('请选择课程分类');
resolve(false);
return false
}
// const textMedia = scheduleMedia.filter((item) => item.mediaType === 'TEXT');
// for (let i = 0, len = textMedia.length; i < len; i++) {
// if (textMedia[i].mediaContentLength && textMedia[i].mediaContentLength.length > 1000) {
// message.warning(`第${i+1}个文字简介的字数超过了1000个字`);
// resolve(false);
// return false
// }
// }
resolve(true);
});
}
render() {
const {
id,
pageType,
courseName,
coverUrl,
studentList,
courseMedia,
introduce,
showCutModal,
showSelectVideoModal,
showSelectImageModal,
diskList,
imageFile,
videoType,
shelfState,
categoryName,
courseCatalogList,
whetherVisitorsJoin,
loadcourseMedia,
loadintroduce,
showSelectCoverModal,
visible,
hasImgReady,
cutImageBlob,
} = this.state;
// 已选择的上课学员数量
const hasSelectedStu = studentList.length;
const courseWareIcon = FileVerifyMap[videoType] ? FileTypeIcon[FileVerifyMap[videoType].type] : FileTypeIcon[videoType];
return (
<div className="page add-video-course-page">
<Breadcrumbs
navList={pageType === "add" ? "新建图文课" : "编辑图文课"}
goBack={this.handleGoBack}
/>
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企培保有依据国家规定及平台规则进行处理的权利" />
</div>
<div className="form">
<div className="course-name required">
<span className="label">课程名称:</span>
<Input
value={courseName}
placeholder="请输入图文课的名称(40字以内)"
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.handleChangeForm('courseName', e.target.value)}}
/>
</div>
<div className="cover-url flex mt16">
<div className="label">封面图:</div>
<div className="cover-url__wrap">
<div className="img-content">
<img src={coverUrl} />
</div>
<div className="opt-btns">
<Button onClick={() => {
this.setState({
showSelectCoverModal: true
});
}}>{`${(pageType === 'add' && !coverUrl) ? '上传' : '修改'}封面`}</Button>
<div className="tips">建议尺寸1280*720px或16:9。封面图最大5M,支持jpg、jpeg和png。</div>
</div>
</div>
</div>
<div className="course-catalog required">
<span className="label">课程分类:</span>
{ (pageType === 'add') &&
<Cascader defaultValue={[categoryName]} options={courseCatalogList} displayRender={ label => label.join('-')} fieldNames={fieldNames} onChange={this.catalogChange} style={{ width: 240 }} placeholder="请选择课程分类" suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}/>
}
{ (pageType === 'edit' && categoryName ) &&
<Cascader defaultValue={[categoryName]} options={courseCatalogList} displayRender={ label => label.join('-')} fieldNames={fieldNames} onChange={this.catalogChange} style={{ width: 240 }} placeholder="请选择课程分类" suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}/>
}
</div>
<div className="intro-info mt16">
<AddGraphicsIntro
data={{
id,
courseMedia,
introduce,
shelfState,
whetherVisitorsJoin,
loadcourseMedia,
loadintroduce,
}}
onChange={this.handleChangeForm}
/>
</div>
</div>
</div>
<div className="footer">
<Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.handleSubmit(), 3000, true)}>保存</Button>
</div>
{/* 选择备课文件弹窗 */}
{ showSelectVideoModal &&
<SelectPrepareFileModal
operateType="select"
selectTypeList={['MP4']}
accept="video/mp4"
confirm={{
title: '文件过大,无法上传',
content: '为保障学员的观看体验,上传的图文大小不能超过2G',
}}
tooltip={'格式支持mp4,大小不超过2G'}
isOpen={showSelectVideoModal}
diskList={diskList}
addVideo={true}
onClose={() => {
this.setState({ showSelectVideoModal: false })
}}
onSelect={this.handleSelectVideo}
/>
}
{showSelectImageModal &&
<SelectPrepareFileModal
key="basic"
operateType="select"
multiple={false}
accept="image/jpeg,image/png,image/jpg"
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectImageModal}
onClose={() => {
this.setState({ showSelectImageModal: false })
}}
onSelect={this.handleSelectImage}
/>
}
{showSelectCoverModal &&
<SelectPrepareFileModal
key="basic"
operateType="select"
multiple={false}
accept="image/jpeg,image/png,image/jpg"
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectCoverModal}
onClose={() => {
this.setState({ showSelectCoverModal: false })
}}
onSelect={this.handleSelectCover}
/>
}
<Modal
title="设置图片"
width={1080}
visible={visible}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
onCancel={() => {
this.setState({ visible: false });
}}
zIndex={10001}
footer={[
<Button
key="back"
onClick={() => {
this.setState({ visible: false });
}}
>
重新上传
</Button>,
<Button
key="submit"
type="primary"
disabled={!hasImgReady}
onClick={() => {
if (!cutFlag) {
cutFlag = true;
this.refs.hiddenBtn.click();
}
this.getSignature(cutImageBlob);
}}
>
确定
</Button>,
]}
>
<div className="clip-box">
<div
id="headPicModal"
ref="headPicModal"
style={{
width: "500px",
height: "430px",
marginBottom: 0,
}}
></div>
<div id="clipBtn" style={{ display: "none" }} ref="hiddenBtn"></div>
<div className="preview-img">
<div className="title">效果预览</div>
<div id="preview-url-box" style={{width:500,height:282}}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt="" />
</div>
</div>
</div>
</Modal>
{ this.state.previewGraphicsModal }
</div>
)
}
}
export default AddGraphicsCourse;
.add-video-course-page {
position:relative !important;
.box{
margin-bottom:66px !important;
}
.ant-radio-group {
display: flex;
flex-direction: column;
.radio-item {
margin-bottom: 12px;
.text {
color: #333;
}
.sub-text {
color: #999;
}
}
.ant-radio {
vertical-align: top;
padding-top: 2px;
}
}
.form {
margin-top: 16px;
padding: 0 16px;
.label{
display:inline-block;
text-align:right;
width:85px;
}
.required {
position: relative;
&::before {
position: absolute;
content: '*';
color: red;
left: 5px;
top: 6px;
}
&.label::before {
top: 0;
}
}
.course-catalog{
margin-bottom:16px;
margin-top:16px;
}
.course-ware {
display: flex;
align-items: center;
margin-bottom: 4px;
&__img {
width: 24px;
margin-right: 4px;
}
&__name {
color: #333;
}
}
.flex {
display: flex;
}
.cover-url__wrap {
.img-content {
width: 298px;
height: 172px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.empty-img {
width: 298px;
height: 172px;
border: 1px dashed #EBEBEB;
border-radius: 4px;
padding: 12px;
color: #999;
padding: 52px 24px;
text-align: center;
}
.opt-btns {
margin-top: 8px;
display: flex;
align-items: center;
.tips {
margin-left: 12px;
color: #999;
}
}
}
.select-student {
align-items: center;
margin-left: 24px;
margin-top: 8px;
.has-selected {
margin-left: 12px;
color: #333;
}
}
.sub-content {
margin-left: 85px;
margin-top: 4px;
.tips {
margin-left: 4px;
color: #999;
}
}
}
.footer {
position: fixed;
bottom: 0;
height: 58px;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 252px;
background: #fff;
border-top: 1px solid #E8E8E8;
z-index: 999;
.ant-btn {
margin-left: 10px;
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-16 11:05:17
* @Last Modified by: chenshu
* @Last Modified time: 2021-03-25 11:25:02
* @Description: 添加直播-简介
*/
import React from 'react';
import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd';
import Service from '@/common/js/service';
import GraphicsEditor from '../../components/GraphicsEditor';
import User from '@/common/js/user';
import UploadOss from '@/core/upload';
import './AddGraphicsIntro.less';
import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepareFileModal';
import { DISK_MAP } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
const { TextArea } = Input;
class AddGraphicsIntro extends React.Component {
constructor(props) {
super(props);
this.state = {
showSelectFileModal: false,
diskList: [],
selectType: null,
}
this.isContent = true;
}
componentDidMount() {
this.bindClick();
}
componentWillUnmount() {
this.removeClick();
}
bindClick = () => {
window.addEventListener('click', this.clickEditor)
}
removeClick = () => {
window.removeEventListener('click', this.clickEditor)
}
clickEditor = (e) => {
if (e && e.target.closest('.content-editor')) {
this.isContent = true
console.log(11111111)
} else if (e && e.target.closest('.introduce-editor')) {
this.isContent = false
console.log(222222222)
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 选择暖场资源
handleSelectVideo = (file) => {
const { selectType } = this.state;
this.setState({
showSelectFileModal: false
})
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
if(selectType === 'WARMUP'){
const liveCourseWarmMedia = {
contentType: 'WARMUP',
mediaType: folderFormat === 'MP4' ? 'VIDEO' : 'PICTURE',
mediaContent: resourceId,
mediaUrl: ossUrl,
mediaName: folderName,
size: folderSize
}
this.props.onChange('liveCourseWarmMedia', liveCourseWarmMedia);
}else{
// 最多添加九图片
const { liveCourseMediaRequests } = this.props.data;
const list = _.filter(liveCourseMediaRequests, (item) => {
return item.mediaType == "PICTURE";
});
if (list.length > 8) {
message.warning("最多添加9张图片");
return;
}
liveCourseMediaRequests.push({
contentType: 'INTRO',
size: folderSize,
mediaName: folderName,
mediaContent: resourceId,
mediaType: 'PICTURE',
mediaUrl: ossUrl,
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
}
changeDetail = (value) => {
this.props.onChange('isContent', !!this.isContent);
setTimeout(() => {
this.props.onChange('courseMedia', value);
}, 0)
}
changeIntro = (value) => {
this.props.onChange('isContent', !!this.isContent);
setTimeout(() => {
this.props.onChange('introduce', value);
}, 0)
}
whetherVisitorsJoinChange = ()=>{
if(this.props.data.whetherVisitorsJoin==="NO"){
this.props.onChange('whetherVisitorsJoin','YES')
}else{
this.props.onChange('whetherVisitorsJoin','NO')
}
}
shelfStateChange = ()=>{
if(this.props.data.shelfState==="NO"){
this.props.onChange('shelfState','YES')
}else{
this.props.onChange('shelfState','NO')
}
}
render() {
const {data: { id, whetherVisitorsJoin, courseMedia, introduce, shelfState, loadcourseMedia, loadintroduce } } = this.props;
const { showSelectFileModal, selectType } = this.state;
return (
<div className="add-video__intro-info">
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch checked={whetherVisitorsJoin==="YES"? true:false} onChange={this.whetherVisitorsJoinChange}/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的用户观看</div>
<div>关闭:仅限绑定了手机号的用户可以进入观看图文课</div>
</div>
</div>
</div>
</div>
<div className="store-show">
<span className="label">店铺展示:</span>
<div className="content">
<Row>
<Col span={3}>
<Switch checked={shelfState==="YES"? true:false} onChange={this.shelfStateChange}/>
</Col>
<Col span={21}>
<div className="desc">
<div>开启:图文课将在用户学院图文课列表中展示</div>
<div>关闭:图文课将在用户学院图文课列表中隐藏</div>
</div>
</Col>
</Row>
</div>
</div>
<div className="introduce required">
<span className="label" style={{ marginTop: 5 }}>课程内容:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item content-editor">
{(!id || loadcourseMedia) &&
<GraphicsEditor
id="content"
detail={{
content: courseMedia
}}
onChange={(val) => { this.changeDetail(val) }}
/>
}
</div>
</div>
</div>
</div>
<div className="introduce">
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!id || loadintroduce) &&
<GraphicsEditor
id="intro"
isIntro={true}
detail={{
content: introduce
}}
onChange={(val) => { this.changeIntro(val) }}
/>
}
</div>
</div>
</div>
</div>
{/* 选择暖场图文件弹窗 */}
{ showSelectFileModal &&
<SelectPrepareFileModal
operateType="select"
accept={selectType==="INTRO"?"image/jpeg,image/png,image/jpg":"video/mp4,image/jpeg,image/png,image/jpg"}
selectTypeList={ selectType==="INTRO" ? ['JPG', 'JPEG', 'PNG']: ['MP4', 'JPG', 'JPEG', 'PNG'] }
tooltip={ selectType==="INTRO"?'支持文件类型:jpg、jpeg、png':'支持文件类型:jpg、jpeg、png、mp4'}
isOpen={showSelectFileModal}
onClose={() => {
this.setState({ showSelectFileModal: false })
}}
onSelect={this.handleSelectVideo}
/>
}
</div>
)
}
}
export default AddGraphicsIntro;
.add-video__intro-info {
.w-e-full-screen-editor {
background: #fff !important;
}
.playback {
margin-bottom: 10px;
.require {
color: #EC4B35;
}
&__text {
color: #999;
}
}
.label{
display:inline-block;
text-align:right;
width:85px;
}
.playback,
.introduce {
display: flex;
flex-direction: row;
}
.allow-tourist-join{
display:flex;
.content{
display:flex;
}
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
}
}
.store-show{
display:flex;
margin-top:16px;
margin-bottom:16px;
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
}
}
.radio {
display: block;
height: 30px;
line-height: 30px;
}
.interactive-playback {
display: flex;
margin-bottom: 20px;
}
textarea.ant-input {
min-height: 80px;
}
.intro-list__item {
display: flex;
margin-bottom: 16px;
position: relative;
&.picture {
width: 550px;
padding: 16px;
border: 1px solid #EEE;
border-radius: 4px;
.img__wrap {
width: 299px;
height: 168px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.little-icon {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
right: -20px;
.iconfont {
width: 20px;
height: 20px;
line-height: 20px;
font-size: 20px;
color: #999;
margin-bottom: 4px;
cursor: pointer;
&.close {
margin-top: 8px;
background-image: url('https://image.xiaomaiketang.com/xm/eesMPhNP3e.png');
background-size: 100% 100%;
}
}
}
}
.operate {
display: flex;
align-items: center;
justify-content: center;
width: 550px;
height: 80px;
line-height: 80px;
padding: 16px;
margin-top: 16px;
border: 1px dashed #EBEBEB;
border-radius: 4px;
.ant-upload {
vertical-align: middle;
}
&__item {
display: flex;
flex-direction: column;
cursor: pointer;
&:not(:last-child) {
margin-right: 82px;
}
.iconfont {
font-size: 22px;
line-height: 22px;
color: #BFBFBF;
text-align: center;
}
.text {
color: #999;
line-height: 20px;
margin-top: 4px;
}
}
}
.tips {
color: #999;
margin-top: 16px;
margin-bottom: 8px;
}
.checkExample {
width: 60px;
color: #FF7519;
cursor: pointer;
}
.warmup {
margin-bottom: 17px;
display: flex;
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
position: relative;
margin-right: 20px;
width: 300px;
height: 170px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.img-delete-wrap {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
transform: translate(-50%, -50%);
}
&:hover {
opacity: 1;
cursor: pointer;
}
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #FF7519;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
}
}
.example-wrap {
font-family: PingFangSC-Regular, PingFang SC;
text-align: center;
.title {
margin-bottom: 6px;
font-size: 14px;
color: #333333;
}
.text {
margin-bottom: 16px;
font-size: 12px;
color: #999999;
}
img {
width: 180px;
}
}
.check-record-rule {
width: 120px;
color: #FF7519;
cursor: pointer;
z-index: 2;
}
.record-rule-wrap {
text-align: left;
ul {
margin-top: 10px;
padding-left: 34px;
list-style-type: disc;
li {
color: #999;
}
}
.text {
color: #999;
}
}
.auto-send-class-report {
.label {
width: 57px;
height: 12px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 12px;
}
.open-text, .close-text {
width: 572px;
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
margin-left: 100px;
margin-top: 5px;
}
.open-text {
margin-top: 8px;
}
.close-text {
margin-bottom: 16px;
}
}
\ No newline at end of file
import React from 'react';
import { Row, Input, Select,Tooltip } from 'antd';
import RangePicker from "@/modules/common/DateRangePicker";
import './GraphicsCourseFilter.less';
import moment from 'moment';
import StoreService from "@/domains/store-domain/storeService";
const { Search } = Input;
const { Option } = Select;
const DEFAULT_QUERY = {
courseName: null, // 课程名称
operatorId: null, // 创建人
beginTime: null, // 开始日期
endTime: null, // 结束日期
shelfState:null,
}
const defaultTeacherQuery = {
size: 10,
current: 1,
nickName:null
}
class GraphicsCourseFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
query: { ...DEFAULT_QUERY }, // 使用扩展运算符,避免浅拷贝
teacherQuery: defaultTeacherQuery,
teacherList:[],
expandFilter:false
}
}
componentDidMount() {
this.getTeacherList();
}
getTeacherList(current = 1, selectList){
const { teacherQuery,teacherList} = this.state;
const _query = {
...teacherQuery,
current,
size:10
};
StoreService.getStoreUserBasicPage( _query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? teacherList.concat(records) : records;
this.setState({
hasNext,
teacherList: list,
})
});
}
// 滑动加载更多讲师列表
handleScrollTeacherList = (e) => {
const { hasNext } = this.state;
const container = e.target;
const scrollToBottom = container && container.scrollHeight <= container.clientHeight + container.scrollTop;
if (scrollToBottom && hasNext) {
const { teacherQuery } = this.state;
let _teacherQuery = teacherQuery;
_teacherQuery.current = _teacherQuery.current + 1
this.setState({
teacherQuery:{..._teacherQuery}
},()=>{this.getTeacherList(_teacherQuery.current)})
}
}
// 改变搜索条件
handleChangeQuery = (field, value) => {
this.setState({
query: {
...this.state.query,
[field]: value,
current: 1,
}
}, () => {
if (field === 'courseName') return;
this.props.onChange(this.state.query)
});
}
handleChangeDates = (dates) => {
const query = _.clone(this.state.query);
if (_.isEmpty(dates)) {
delete query.beginTime;
delete query.endTime;
} else {
query.beginTime = dates[0].valueOf();
query.endTime = dates[1].valueOf();
}
this.setState({
query:{
...query,
current: 1,
}
}, () => {
this.props.onChange(this.state.query);
})
}
// 重置搜索条件
handleReset = () => {
this.setState({
query: DEFAULT_QUERY,
}, () => {
this.props.onChange(this.state.query);
})
}
render() {
const {
query: {
courseName,
operator,
beginTime,
endTime,
operatorId,
shelfState
},
expandFilter,
teacherList,
teacherQuery
} = this.state;
return (
<div className="video-course-filter">
<Row type="flex" justify="space-between" align="top">
<div className="search-condition">
<div className="search-condition__item">
<span className="search-name">图文课名称:</span>
<Search
value={courseName}
placeholder="搜索图文课名称"
onChange={(e) => { this.handleChangeQuery('courseName', e.target.value)}}
onSearch={ () => { this.props.onChange(this.state.query) } }
style={{ width: "calc(100% - 84px)" }}
enterButton={<span className="icon iconfont">&#xe832;</span>}
/>
</div>
<div className="search-condition__item">
<span>创建人:</span>
<Select
placeholder="请选择创建人"
style={{width:"calc(100% - 70px)"}}
showSearch
allowClear
filterOption={(input, option) => option}
onPopupScroll={this.handleScrollTeacherList}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
value={operatorId}
onChange={(value) => {
this.handleChangeQuery('operatorId', value)
}}
onSearch={(value) => {
teacherQuery.nickName = value
this.setState({
teacherQuery
}, () => {
this.getTeacherList()
})
}}
onClear ={(value)=>{
this.setState({
teacherQuery:{
size: 10,
current: 1,
nickName:null
}
}, () => {
this.getTeacherList()
})
}
}
>
{_.map(teacherList, (item, index) => {
return (
<Select.Option value={item.id} key={item.id}>{item.nickName}</Select.Option>
);
})}
</Select>
</div>
<div className="search-condition__item">
<span className="search-date">创建日期:</span>
<RangePicker
id="course_date_picker"
allowClear={false}
value={ beginTime ? [moment(beginTime), moment(endTime)] : null }
format={"YYYY-MM-DD"}
onChange={(dates) => { this.handleChangeDates(dates) }}
style={{ width: "calc(100% - 70px)" }}
/>
</div>
{ expandFilter &&
<div className="search-condition__item">
<span className="shelf-status">店铺展示:</span>
<Select
style={{ width: "calc(100% - 84px)" }}
placeholder="请选择"
allowClear={true}
value={shelfState}
onChange={(value) => { this.handleChangeQuery('shelfState', value) }}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
>
<Option value="YES">开启</Option>
<Option value="NO">关闭</Option>
</Select>
</div>
}
</div>
<div className="reset-fold-area">
<Tooltip title="清空筛选"><span className="resetBtn iconfont icon" onClick={this.handleReset}>&#xe61b; </span></Tooltip>
<span style={{ cursor: 'pointer' }} className="fold-btn" onClick={() => {
this.setState({expandFilter:!expandFilter});
}}>{this.state.expandFilter ? <span><span>收起</span><span className="iconfont icon fold-icon" >&#xe82d; </span> </span> : <span>展开<span className="iconfont icon fold-icon" >&#xe835; </span></span>}</span>
</div>
</Row>
</div>
)
}
}
export default GraphicsCourseFilter;
.video-course-filter {
position: relative;
.video-list-table{
// tr:nth-child(even){
// background: transparent !important;
// }
// tr:nth-child(odd){
// td{
// background: #FAFAFA !important;
// }
// }
}
.search-condition {
width: calc(100% - 80px);
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
margin-bottom: 12px;
align-items: center;
display: flex;
.search-name{
vertical-align: middle;
}
.shelf-status{
width:84px;
display:inline-block;
text-align:right;
}
}
}
.reset-fold-area {
position: absolute;
right: 12px;
}
.resetBtn {
color: #999999;
font-size: 18px;
margin-right: 8px;
}
.fold-btn {
font-size: 14px;
color: #666666;
line-height: 20px;
.fold-icon {
font-size: 12px;
margin-left:4px;
}
}
}
.data-icon {
cursor: pointer;
}
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-02-01 16:34:11
* @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Table, Modal, message , Tooltip,Switch,Dropdown} from 'antd';
import { PageControl } from "@/components";
import { LIVE_SHARE_MAP } from '@/common/constants/academic/cloudClass';
import { appId, shareUrl, LIVE_SHARE } from '@/domains/course-domain/constants';
import ShareLiveModal from '@/modules/course-manage/modal/ShareLiveModal';
import WatchDataModal from '../modal/WatchDataModal'
import CourseService from "@/domains/course-domain/CourseService";
import RelatedPlanModal from '../../modal/RelatedPlanModal';
import User from '@/common/js/user'
import './GraphicsCourseList.less';
const ENV = process.env.DEPLOY_ENV || 'dev';
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class GraphicsCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
id: '', // 视频课ID
studentIds:[]
}
}
componentDidMount() {
const videoCourseItem = localStorage.getItem('videoCourseItem');
if (videoCourseItem) {
const _videoCourseItem = JSON.parse(videoCourseItem);
this.handleShowShareModal(_videoCourseItem, true);
}
}
// 观看数据弹窗
handleShowWatchDataModal = (record) => {
const watchDataModal = (
<WatchDataModal
type='videoCourseList'
data={record}
close={() => {
this.setState({
watchDataModal: null
});
}}
/>
);
this.setState({ watchDataModal });
}
// 请求表头
parseColumns = () => {
const columns = [
{
title: '图文课',
key: 'scheduleName',
dataIndex: 'scheduleName',
width:321,
fixed: 'left',
render: (val, record) => {
const { coverUrl, scheduleVideoUrl } = record;
return (
<div className="record__item">
{/* 上传了封面的话就用上传的封面, 没有的话就取视频的第一帧 */}
<img className="course-cover" src={coverUrl || defaultCoverUrl} />
{ record.courseName.length > 25?
<Tooltip title={record.courseName}>
<div className="course-name">{record.courseName}</div>
</Tooltip>
:
<div className="course-name">{record.courseName}</div>
}
</div>
)
}
},
{
title: '课程分类',
key: 'categoryName',
dataIndex: 'categoryName',
width: '20%',
render: (val, record) => {
return (
<div className="record__item">
{record.categoryOneName}{ record.categoryTwoName?`-${record.categoryTwoName}`:''}
</div>
)
}
},
{
title: '创建人',
key: 'createName',
dataIndex: 'createName',
width: '10%',
render: (val) => {
return (
<div>
{ val &&
<Tooltip title={val}>
<div>
{val.length > 4 ? `${val.slice(0,4)}...` : val}
</div>
</Tooltip>
}
</div>
)
}
},
{
title: <span>
<span>店铺展示</span>
<Tooltip title={<div>开启后,用户可在店铺内查看到此课程。若课程“未成功开课”,则系统会自动“关闭”店铺展示。<br/>关闭后,店铺内不再展示此课程,但用户仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
width: '12%',
dataIndex: "courseware",
render: (val, item, index) => {
return (
<Switch defaultChecked={item.shelfState==="YES"?true:false} onChange={()=>this.changeShelfState(item)}/>
)
},
},
{
title: "观看用户数",
width: 110,
key: "watchUserCount",
dataIndex: "watchUserCount",
render: (val, item) => {
return (
<div className="watchUserCount">{val}</div>
)
},
},
{
title: '创建时间',
width: 181,
key: 'created',
dataIndex: 'created',
sorter: true,
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '最近修改时间',
width: 181,
key: 'updated',
dataIndex: 'updated',
sorter: true,
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '操作',
key: 'operate',
dataIndex: 'operate',
width: 210,
fixed: 'right',
render: (val, record) => {
return (
<div className="operate">
<div className="operate__item" onClick={()=>this.handleShowWatchDataModal(record)}>观看数据</div>
<span className="operate__item split"> | </span>
<div className="operate__item" onClick={() => this.handleShowShareModal(record)}>分享</div>
<span className="operate__item split"> | </span>
<Dropdown overlay={this.renderMoreOperate(record)}>
<span className="more-operate">
<span className="operate-text">更多</span>
<span
className="iconfont icon"
style={{ color: "#5289FA" }}
>
&#xe824;
</span>
</span>
</Dropdown>
</div>
)
}
}
];
return columns;
}
handleRelatedModalShow = (item) => {
const selectPlanList = {};
if(item.relatedPlanList){
item.relatedPlanList.map((item, index) => {
selectPlanList[item.planId] = {}
selectPlanList[item.planId].planId = item.planId;
selectPlanList[item.planId].taskBaseVOList = [{ taskId: item.taskId }];
return item
})
}
this.setState({
RelatedPlanModalVisible: true,
selectCourseId: item.id,
selectPlanList: selectPlanList,
})
}
closeRelatedPlanModalVisible = ()=>{
this.setState({
RelatedPlanModalVisible: false
})
}
onChangeSelectPlanList = (selectPlanList) => {
this.setState({
selectPlanList: selectPlanList
})
}
onConfirmSelectPlanList = () => {
this.setState({
RelatedPlanModalVisible: false
}, () => { this.props.onChange(); });
}
renderMoreOperate = (item) => {
return (
<div className="live-course-more-menu">
{/* {(User.getUserRole() === "CloudManager" || User.getUserRole() === "StoreManager") &&
<div
className="operate__item"
key="plan"
onClick={() => {
this.handleRelatedModalShow(item);
}}
>关联培训计划</div>
} */}
<div
className="operate__item"
key="edit"
onClick={() => {
RCHistory.push(`/create-graphics-course?type=edit&id=${item.id}`);
}}
>编辑</div>
<div
className="operate__item"
key="delete"
onClick={() => this.handleDeleteGraphicsCourse(item.id)}
>删除</div>
</div>
)
}
//改变上架状态
changeShelfState = (item) =>{
let _shelfState = item.shelfState
if(_shelfState==='NO'){
_shelfState = "YES";
item.shelfState = "YES"
}else{
_shelfState = "NO"
item.shelfState = "NO"
}
const params={
courseId: item.id,
shelfState:_shelfState
}
CourseService.changeVideoShelfState(params).then((res)=>{
if(res.success){
if(_shelfState === "YES"){
message.success("已开启展示");
}else{
message.success("已取消展示");
}
}
})
}
// 删除视频课
handleDeleteGraphicsCourse = (scheduleId) => {
Modal.confirm({
title: '你确定要删除此视频课吗?',
content: '删除后,学员将不能进行观看。',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const param ={
courseId:scheduleId,
storeId:User.getStoreId()
}
CourseService.delVideoSchedule(
param
).then(() => {
message.success('删除成功');
this.props.onChange();
})
}
});
}
// 显示分享弹窗
handleShowShareModal = (record, needStr = false) => {
const { id, scheduleVideoUrl } = record;
const _appId = appId;
const htmlUrl = `${LIVE_SHARE}graphics_detail/${id}?id=${User.getStoreId()}`;
const longUrl = htmlUrl;
const { coverUrl, courseName } = record;
const shareData = {
longUrl,
coverUrl,
scheduleVideoUrl,
courseName,
};
const shareLiveModal = (
<ShareLiveModal
needStr={needStr}
data={shareData}
type="videoClass"
title="图文课"
close={() => {
this.setState({
shareLiveModal: null
});
localStorage.setItem('videoCourseItem', '');
}}
/>
);
this.setState({ shareLiveModal });
}
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
const { query } = this.props;
let { order: _order } =query;
// 按创建时间升序排序
if (columnKey === 'created' && order === 'ascend') { _order = 'CREATED_ASC'; }
// 按创建时间降序排序
if (columnKey === 'created' && order === 'descend') { _order = 'CREATED_DESC'; }
// 按更新时间升序排序
if (columnKey === 'updated' && order === 'ascend') { _order = 'UPDATED_ASC'; }
// 按更新时间降序排序
if (columnKey === 'updated' && order === 'descend') { _order = 'UPDATED_DESC'; }
const _query = {
...query,
orderEnum: _order
};
this.props.onChange(_query);
}
render() {
const { RelatedPlanModalVisible, selectCourseId, selectPlanList } = this.state;
const { dataSource = [], totalCount, query } = this.props;
const { current, size } = query;
return (
<div className="video-course-list">
<Table
rowKey={record => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
onChange={this.handleChangeTable}
pagination={false}
scroll={{ x: 1500}}
bordered
className="video-list-table"
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = {...query, current: page + 1};
this.props.onChange(_query)
}}
/>
</div>
{RelatedPlanModalVisible &&
<RelatedPlanModal
onClose={this.closeRelatedPlanModalVisible}
visible={RelatedPlanModalVisible}
selectCourseId={selectCourseId}
selectPlanList={selectPlanList}
onChange={this.onChangeSelectPlanList}
onConfirm={this.onConfirmSelectPlanList}
/>
}
{ this.state.shareLiveModal }
{ this.state.watchDataModal }
</div>
)
}
}
export default GraphicsCourseList;
.video-course-list {
margin-top: 12px;
.video-list-table{
tbody {
tr{
&:nth-child(even){
background: transparent !important;
td{
background:#FFF !important;
}
}
&:nth-child(odd){
background: #FAFAFA !important;
td{
background: #FAFAFA !important;
}
}
&:hover{
td{
background:#F3f6fa !important;
}
}
}
}
}
.watchUserCount{
text-align:right;
padding:16px;
}
.operate-text {
color: #5289FA;
cursor: pointer;
}
.operate {
display: flex;
&__item {
color: #5289FA;
cursor: pointer;
&.split {
margin: 0 8px;
color: #BFBFBF;
}
}
}
.more-operate{
line-height:20px;
}
.record__item {
display: flex;
.course-cover {
min-width: 97px;
max-width: 97px;
height: 50px;
border-radius: 2px;
margin-right: 8px;
background-color: #666;
}
.course-name {
color: #666;
width:188px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
height:48px;
}
}
}
.video-course-more-menu {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 4px;
div {
line-height: 30px;
padding: 0 15px;
cursor: pointer;
&:hover {
background: #f3f6fa;
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-26 16:07:27
* @Description: 视频课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Button } from 'antd';
import './GraphicsCourseOpt.less';
export default function GraphicsCourseOpt() {
return (
<div className="video-course-opt">
<Button
type="primary"
onClick={() => {
RCHistory.push('/create-graphics-course?type=add');
}}
className="mr12"
>新建图文课</Button>
</div>
);
}
.video-course-opt {
margin-top:4px;
.link {
color: #FF8534;
}
}
\ No newline at end of file
import React from 'react';
import GraphicsCourseFilter from './components/GraphicsCourseFilter';
import GraphicsCourseOpt from './components/GraphicsCourseOpt';
import GraphicsCourseList from './components/GraphicsCourseList';
import Service from '@/common/js/service';
import User from '@/common/js/user'
class GraphicsCourse extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
size: 10,
current: 1,
courseType: 'PICTURE',
storeId:User.getStoreId()
},
dataSource: [], // 视频课列表
totalCount: 0, // 视频课数据总条数
}
}
componentWillMount() {
// 获取视频课列表
this.handleFetchScheduleList();
}
// 获取视频课列表
handleFetchScheduleList = (_query = {}) => {
const query = {
...this.state.query,
..._query
};
// 更新请求参数
this.setState({ query });
Service.Hades('public/hades/mediaCoursePage', query).then((res) => {
const { result = {} } = res || {};
const { records = [], total = 0 } = result;
this.setState({
dataSource: records,
totalCount: Number(total)
});
})
// CourseService.videoSchedulePage(query).then((res) => {
// const { result = {} } = res || {};
// const { records = [], total = 0 } = result;
// this.setState({
// dataSource: records,
// totalCount: Number(total)
// });
// });
}
render() {
const { dataSource, totalCount, query } = this.state;
return (
<div className="page video-course-page">
<div className="content-header">图文课</div>
<div className="box">
{/* 搜索模块 */}
<GraphicsCourseFilter
onChange={this.handleFetchScheduleList}
/>
{/* 操作模块 */}
<GraphicsCourseOpt />
{/* 视频课列表模块 */}
<GraphicsCourseList
query={query}
dataSource={dataSource}
totalCount={totalCount}
onChange={this.handleFetchScheduleList}
/>
</div>
</div>
)
}
}
export default GraphicsCourse;
/*
* @Author: 吴文洁
* @Date: 2020-05-19 11:01:31
* @Last Modified by: chenshu
* @Last Modified time: 2021-03-24 15:13:38
* @Description 余额异常弹窗
*/
import React from 'react';
import {Table, Modal,Input} from 'antd';
import { PageControl } from "@/components";
import Service from "@/common/js/service";
import User from '@/common/js/user'
import './WatchDataModal.less';
import dealTimeDuration from "../../utils/dealTimeDuration";
const { Search } = Input;
class WatchDataModal extends React.Component {
constructor(props) {
super(props);
this.state = {
visible:true,
dataSource:[],
size:10,
query: {
current: 1,
},
totalCount:0
};
}
componentDidMount() {
this.handleFetchDataList();
}
onClose = () =>{
this.props.close();
}
// 获取观看视频数据列表
handleFetchDataList = () => {
const {query,size,totalCount} = this.state
const { id } = this.props.data;
const params ={
...query,
size,
courseId:id,
storeId:User.getStoreId()
}
Service.Hades('public/hades/mediaCourseWatchInfo', params).then((res) => {
const { result = {} } = res ;
const { records = [], total = 0 } = result;
this.setState({
dataSource: records,
totalCount: Number(total)
});
});
}
handleChangNickname = (value)=>{
const isPhone = (value || '').match(/^\d+$/);
const { query } = this.state;
if(isPhone){
query.phone = value;
query.nickName = null;
}else{
query.nickName = value;
query.phone = null;
}
query.current = 1;
this.setState({
query
})
}
onShowSizeChange = (current, size) => {
if (current == size) {
return
}
this.setState({
size
},()=>{this.handleFetchDataList()})
}
// 请求表头
parseColumns = () => {
const columns = [
{
title: '观看用户',
key: 'name',
dataIndex: 'name'
},
{
title: '手机号',
key: 'phone',
dataIndex: 'phone'
},
{
title: '观看者类型',
key: 'userRole',
dataIndex: 'userRole'
},
{
title: '首次观看时间',
key: 'firstWatch',
dataIndex: 'firstWatch',
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '观看总时长',
key: 'watchDuration',
dataIndex: 'watchDuration',
render: (val) => {
return <span>{val ? dealTimeDuration(val) : "00:00:00" }</span>
}
},
{
title: '学习进度',
key: 'progress',
dataIndex: 'progress',
render: (val) => {
return <span>{val === 100 ? '已完成' : `${val || 0}%`}</span>
}
}
];
return columns;
}
render() {
const { visible,size,dataSource,totalCount,query} = this.state;
return (
<Modal
title="图文课观看数据"
visible={visible}
footer={null}
onCancel={this.onClose}
maskClosable={false}
className="watch-data-modal"
closable={true}
width={800}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
>
<div className="search-container">
<Search placeholder="搜索用户姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
</div>
<div>
<Table
rowKey={record => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
pagination={false}
bordered
/>
{dataSource.length >0 &&
<div className="box-footer">
<PageControl
current={query.current - 1}
pageSize={size}
total={totalCount}
size="small"
toPage={(page) => {
const _query = {...query, current: page + 1};
this.setState({
query:_query
},()=>{ this.handleFetchDataList()})
}}
onShowSizeChange={this.onShowSizeChange}
/>
</div>
}
</div>
</Modal>
)
}
}
export default WatchDataModal;
\ No newline at end of file
.watch-data-modal{
.search-container{
text-align:right;
margin-bottom:17px;
}
}
\ No newline at end of file
...@@ -76,7 +76,7 @@ class PreviewCourseModal extends React.Component { ...@@ -76,7 +76,7 @@ class PreviewCourseModal extends React.Component {
const { courseBasicInfo, courseClassInfo = {}, courseIntroInfo, type,courseState,origin} = this.props; const { courseBasicInfo, courseClassInfo = {}, courseIntroInfo, type,courseState,origin} = this.props;
const { coverUrl, courseName, scheduleVideoUrl,videoDuration} = courseBasicInfo; const { coverUrl, courseName, scheduleVideoUrl,videoDuration} = courseBasicInfo;
const { liveDate, calendarTime,startTime,endTime,timeHorizonStart, timeHorizonEnd, teacherName } = courseClassInfo; const { liveDate, calendarTime,startTime,endTime,timeHorizonStart, timeHorizonEnd, teacherName } = courseClassInfo;
const { liveCourseMediaRequests } = courseIntroInfo; const { introduce } = courseIntroInfo;
let liveDateStr, startTimeStr, endTimeStr; let liveDateStr, startTimeStr, endTimeStr;
...@@ -177,28 +177,12 @@ class PreviewCourseModal extends React.Component { ...@@ -177,28 +177,12 @@ class PreviewCourseModal extends React.Component {
<div className="container__introduction__title">直播课简介</div> <div className="container__introduction__title">直播课简介</div>
} }
<div className="container__introduction__list editor-box"> <div className="container__introduction__list editor-box">
{
liveCourseMediaRequests.map((item, index) => {
if (item.mediaType === 'TEXT') {
return (
<div <div
className="intro-item text" className="intro-item text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: item.mediaContent __html: introduce
}} }}
/> />
)
}
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-item picture">
<img src={item.mediaUrl} />
</div>
)
}
})
}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -122,21 +122,16 @@ ...@@ -122,21 +122,16 @@
margin-top: 13px; margin-top: 13px;
} }
.text {
color: #666; color: #666;
line-height: 17px;
p { p {
font-size: 12px; font-size: 12px;
} }
}
.picture {
img { img {
width: 100%; width: 100%;
} }
} }
} }
} }
}
} }
\ No newline at end of file
import React from 'react';
import { Modal } from 'antd';
import './PreviewGraphicsModal.less';
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class PreviewGraphicsModal extends React.Component {
constructor(props) {
super(props);
this.state = {
type: 'detail',
}
}
render() {
const { courseBasicInfo, courseIntroInfo } = this.props;
const { coverUrl, courseName, categoryName } = courseBasicInfo;
const { courseMedia, introduce } = courseIntroInfo;
const { type } = this.state;
return (
<Modal
title="预览"
visible={true}
width={680}
onCancel={this.props.close}
footer={null}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
className="preview-live-graphics-modal"
>
<div className="container__wrap">
<div className="container">
<div className="container__header">
<img src={coverUrl || defaultCoverUrl} className="course-cover" />
</div>
<div className="container__body">
<div className="title__name">{courseName}</div>
<div className="title__categery">课程分类:{categoryName}</div>
</div>
<div className="container__introduction">
<div className="title">
<span
className={`title-word${type === 'detail' ? ' selected' : ''}`}
onClick={() => this.setState({ type: 'detail' })}
>图文详情</span>
<span
className={`title-word${type === 'introduction' ? ' selected' : ''}`}
onClick={() => this.setState({ type: 'introduction' })}
>图文简介</span>
</div>
<div className="container__introduction__list editor-box">
<div
className="intro-item text"
dangerouslySetInnerHTML={{
__html: type === 'detail' ? courseMedia : introduce
}}
/>
</div>
</div>
</div>
</div>
</Modal>
)
}
}
export default PreviewGraphicsModal;
.preview-live-graphics-modal {
.ant-modal-body {
background-image: url('https://image.xiaomaiketang.com/xm/xZWdziTCAf.png');
background-size: 100% 100%;
}
.container__wrap {
width: 340px;
height: 618px;
padding: 67px 46px 48px 47px;
margin: auto;
background-image: url('https://image.xiaomaiketang.com/xm/DHMzHiGc2E.png');
background-size: 100% 100%;
}
.container {
overflow: scroll;
height: 100%;;
.course-cover, .course-url {
width: 100%;
height: 141px;
background: #000;
}
&__body {
background-color: #FFF;
padding: 7px 0 11px 0;;
.title__name {
color: #333333;
font-weight: 500;
}
.title__categery {
font-size: 12px;
color: #999999;
}
}
&__introduction {
margin-top: 10px;
padding: 12px 0;
position: relative;
&::after {
content: '';
position: absolute;
width: 241px;
top: -10px;
height: 10px;
background: #F4F6FA;
}
.title {
height: 24px;
display: flex;
align-items: center;
font-size: 12px;
color: #333333;
padding: 0 10px;
border-bottom: 1px solid #E8E8E8;
.title-word {
position: relative;
margin-right: 15px;
cursor: pointer;
}
.selected {
color: #FFB714;
&::after {
content: '';
position: absolute;
bottom: -4px;
width: 20px;
height: 1px;
background: #FFB714;
left: 50%;
transform: translateX(-50%);
}
}
}
&__list {
margin-top: 12px;
.intro-item:not(:first-child) {
margin-top: 13px;
}
color: #666;
p {
font-size: 12px;
}
img {
max-width: 100%;
}
}
}
}
}
\ No newline at end of file
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-07-20 19:12:49 * @Date: 2020-07-20 19:12:49
* @Last Modified by: 吴文洁 * @Last Modified by: chenshu
* @Last Modified time: 2020-07-20 20:25:13 * @Last Modified time: 2021-03-16 17:41:40
* @Description: 大班直播分享弹窗 * @Description: 大班直播分享弹窗
*/ */
...@@ -130,7 +130,7 @@ class ShareLiveModal extends React.Component { ...@@ -130,7 +130,7 @@ class ShareLiveModal extends React.Component {
} }
render() { render() {
const { needStr, data, type } = this.props; const { needStr, data, type, title } = this.props;
const { courseName, coverUrl = DEFAULT_COVER, scheduleVideoUrl } = data; const { courseName, coverUrl = DEFAULT_COVER, scheduleVideoUrl } = data;
const { shareUrl ,imgData,showImg,time} = this.state; const { shareUrl ,imgData,showImg,time} = this.state;
...@@ -139,7 +139,7 @@ class ShareLiveModal extends React.Component { ...@@ -139,7 +139,7 @@ class ShareLiveModal extends React.Component {
let coverImgSrc = coverUrl; let coverImgSrc = coverUrl;
if(type === 'videoClass'){ if(type === 'videoClass'){
if(!coverUrl || isDefaultCover){ if((!coverUrl || isDefaultCover) && title !== '图文课'){
coverImgSrc = `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring` coverImgSrc = `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring`
} }
}else{ }else{
...@@ -153,7 +153,7 @@ class ShareLiveModal extends React.Component { ...@@ -153,7 +153,7 @@ class ShareLiveModal extends React.Component {
return ( return (
<Modal <Modal
title={type === 'videoClass' ? '分享视频课' : '分享直播课'} title={`分享${title}`}
width={680} width={680}
visible={true} visible={true}
footer={null} footer={null}
...@@ -197,10 +197,10 @@ class ShareLiveModal extends React.Component { ...@@ -197,10 +197,10 @@ class ShareLiveModal extends React.Component {
<div className="share-poster right__item"> <div className="share-poster right__item">
<div className="title">① 海报分享</div> <div className="title">① 海报分享</div>
{ type === "liveClass" && { type === "liveClass" &&
<div className="sub-title">用户可通过微信扫描海报二维码,观看直播</div> <div className="sub-title">用户可通过微信扫描海报二维码,观看{title}</div>
} }
{ type === "videoClass" && { type === "videoClass" &&
<div className="sub-title">用户可通过微信识别二维码,报名观看视频</div> <div className="sub-title">用户可通过微信识别二维码,报名观看{title}</div>
} }
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div> <div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
...@@ -209,10 +209,10 @@ class ShareLiveModal extends React.Component { ...@@ -209,10 +209,10 @@ class ShareLiveModal extends React.Component {
<div className="share-url right__item"> <div className="share-url right__item">
<div className="title">② 链接分享</div> <div className="title">② 链接分享</div>
{ type === "liveClass" && { type === "liveClass" &&
<div className="sub-title">用户可通过微信打开以下链接,观看直播</div> <div className="sub-title">用户可通过微信打开以下链接,观看{title}</div>
} }
{ type === "videoClass" && { type === "videoClass" &&
<div className="sub-title">用户可通过打开链接,报名观看视频</div> <div className="sub-title">用户可通过打开链接,报名观看{title}</div>
} }
<div className="content url-content"> <div className="content url-content">
<div className="share-url" id="shareUrl">{shareUrl}</div> <div className="share-url" id="shareUrl">{shareUrl}</div>
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-08-05 10:07:47 * @Date: 2020-08-05 10:07:47
* @LastEditors: zhangleyuan * @LastEditors: yuananting
* @LastEditTime: 2021-03-12 10:12:23 * @LastEditTime: 2021-03-24 19:34:37
* @Description: 视频课新增/编辑页 * @Description: 视频课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有 * @Copyright: 杭州杰竞科技有限公司 版权所有
*/ */
...@@ -21,9 +21,13 @@ import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFile ...@@ -21,9 +21,13 @@ import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFile
import PreviewCourseModal from '../modal/PreviewCourseModal'; import PreviewCourseModal from '../modal/PreviewCourseModal';
import StoreService from "@/domains/store-domain/storeService"; import StoreService from "@/domains/store-domain/storeService";
import CourseService from "@/domains/course-domain/CourseService"; import CourseService from "@/domains/course-domain/CourseService";
import Service from '@/common/js/service';
import User from '@/common/js/user'; import User from '@/common/js/user';
import _ from "underscore"; import _ from "underscore";
import Upload from '@/core/upload'; import Upload from '@/core/upload';
import { randomString } from '@/domains/basic-domain/utils';
import $ from 'jquery';
import Bus from '../../../core/bus'
// import PhotoClip from 'photoclip'; // import PhotoClip from 'photoclip';
import './AddVideoCourse.less'; import './AddVideoCourse.less';
...@@ -76,6 +80,7 @@ class AddVideoCourse extends React.Component { ...@@ -76,6 +80,7 @@ class AddVideoCourse extends React.Component {
whetherVisitorsJoin:'NO', // 是否允许游客加入 whetherVisitorsJoin:'NO', // 是否允许游客加入
showSelectCoverModal:false, showSelectCoverModal:false,
cutImageBlob: null, cutImageBlob: null,
introduce: '',
} }
} }
...@@ -85,7 +90,21 @@ class AddVideoCourse extends React.Component { ...@@ -85,7 +90,21 @@ class AddVideoCourse extends React.Component {
if (pageType === 'edit') { if (pageType === 'edit') {
this.handleFetchScheudleDetail(id); this.handleFetchScheudleDetail(id);
} }
this.initBus()
} }
initBus = () => {
Bus.bind('graphicsEditorImage', this.uploadIntroImage)
}
removeBus = () => {
Bus.unbind('graphicsEditorImage', this.uploadIntroImage)
}
uploadIntroImage = () => {
this.setState({ showSelectImageModal: true })
}
//获取分类列表 //获取分类列表
getCourseCatalogList = ()=>{ getCourseCatalogList = ()=>{
StoreService.getCourseCatalogList({current:1,size:1000}).then((res) => { StoreService.getCourseCatalogList({current:1,size:1000}).then((res) => {
...@@ -132,6 +151,7 @@ class AddVideoCourse extends React.Component { ...@@ -132,6 +151,7 @@ class AddVideoCourse extends React.Component {
let scheduleMedia = []; let scheduleMedia = [];
let scheduleVideoId; let scheduleVideoId;
let scheduleVideoUrl; let scheduleVideoUrl;
let hasIntro;
courseMediaVOS.map((item) => { courseMediaVOS.map((item) => {
switch (item.contentType){ switch (item.contentType){
...@@ -147,7 +167,8 @@ class AddVideoCourse extends React.Component { ...@@ -147,7 +167,8 @@ class AddVideoCourse extends React.Component {
videoType = item.mediaType; videoType = item.mediaType;
break; break;
case "INTRO": case "INTRO":
scheduleMedia = [...scheduleMedia,item] hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break; break;
default: default:
break; break;
...@@ -162,6 +183,7 @@ class AddVideoCourse extends React.Component { ...@@ -162,6 +183,7 @@ class AddVideoCourse extends React.Component {
categoryName = `${categoryOneName}`; categoryName = `${categoryOneName}`;
} }
this.setState({ this.setState({
loadintroduce: !hasIntro,
coverId, coverId,
coverUrl, coverUrl,
videoType, videoType,
...@@ -179,6 +201,18 @@ class AddVideoCourse extends React.Component { ...@@ -179,6 +201,18 @@ class AddVideoCourse extends React.Component {
}) })
} }
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ [key]: res, [`load${key}`]: true });
}
})
}
handleGoBack = () => { handleGoBack = () => {
const { const {
...@@ -259,8 +293,8 @@ class AddVideoCourse extends React.Component { ...@@ -259,8 +293,8 @@ class AddVideoCourse extends React.Component {
scheduleVideoUrl, scheduleVideoUrl,
courseName, courseName,
scheduleMedia, scheduleMedia,
videoDuration videoDuration,
introduce,
} = this.state; } = this.state;
const courseBasinInfo = { const courseBasinInfo = {
...@@ -270,7 +304,8 @@ class AddVideoCourse extends React.Component { ...@@ -270,7 +304,8 @@ class AddVideoCourse extends React.Component {
videoDuration videoDuration
} }
const courseIntroInfo = { const courseIntroInfo = {
liveCourseMediaRequests: scheduleMedia liveCourseMediaRequests: scheduleMedia,
introduce,
} }
const previewCourseModal = ( const previewCourseModal = (
...@@ -330,13 +365,14 @@ class AddVideoCourse extends React.Component { ...@@ -330,13 +365,14 @@ class AddVideoCourse extends React.Component {
scheduleVideoUrl, scheduleVideoUrl,
categoryId, categoryId,
shelfState, shelfState,
whetherVisitorsJoin whetherVisitorsJoin,
introduce,
} = this.state; } = this.state;
const commonParams = { const commonParams = {
videoName, videoName,
videoDuration, videoDuration,
scheduleVideoId, courseMediaId: scheduleVideoId,
scheduleMedia: scheduleMedia.filter(item => !!item.mediaContent), scheduleMedia: scheduleMedia.filter(item => !!item.mediaContent),
categoryId, categoryId,
courseName, courseName,
...@@ -344,13 +380,21 @@ class AddVideoCourse extends React.Component { ...@@ -344,13 +380,21 @@ class AddVideoCourse extends React.Component {
operatorId:User.getStoreUserId(), operatorId:User.getStoreUserId(),
storeId:User.getStoreId(), storeId:User.getStoreId(),
shelfState, shelfState,
whetherVisitorsJoin whetherVisitorsJoin,
courseType: 'VOICE',
}; };
// 校验必填字段:课程名称, 课程视频 // 校验必填字段:课程名称, 课程视频
this.handleValidate(courseName, scheduleVideoId,categoryId, scheduleMedia).then((res) => { this.handleValidate(courseName, scheduleVideoId,categoryId, scheduleMedia).then((res) => {
if (!res) return; if (!res) return;
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({ id, pageType, commonParams: { ...commonParams, introduceId } })
});
});
}
submitRemote = ({ id, pageType, commonParams }) => {
if (pageType === 'add') { if (pageType === 'add') {
CourseService.createVideoSchedule(commonParams).then((res) => { Service.Hades('public/hades/createMediaCourse', commonParams).then((res) => {
if (!res) return; if (!res) return;
message.success("新建成功"); message.success("新建成功");
window.RCHistory.push({ window.RCHistory.push({
...@@ -362,7 +406,7 @@ class AddVideoCourse extends React.Component { ...@@ -362,7 +406,7 @@ class AddVideoCourse extends React.Component {
courseId:id, courseId:id,
...commonParams, ...commonParams,
} }
CourseService.editVideoSchedule(editParams).then((res) => { Service.Hades('public/hades/editMediaCourse', editParams).then((res) => {
if (!res) return; if (!res) return;
message.success("保存成功"); message.success("保存成功");
window.RCHistory.push({ window.RCHistory.push({
...@@ -370,7 +414,6 @@ class AddVideoCourse extends React.Component { ...@@ -370,7 +414,6 @@ class AddVideoCourse extends React.Component {
}); });
}); });
} }
});
} }
handleValidate = (courseName, scheduleVideoId,categoryId,scheduleMedia) => { handleValidate = (courseName, scheduleVideoId,categoryId,scheduleMedia) => {
...@@ -405,6 +448,17 @@ class AddVideoCourse extends React.Component { ...@@ -405,6 +448,17 @@ class AddVideoCourse extends React.Component {
this.uploadImage(file); this.uploadImage(file);
} }
handleSelectImage = (file) => {
this.setState({
showSelectImageModal: false
})
const { ossUrl } = file;
const { introduce } = this.state;
this.setState({
introduce: `${introduce}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`
});
}
//上传图片 //上传图片
uploadImage = (imageFile) => { uploadImage = (imageFile) => {
const { folderName } = imageFile; const { folderName } = imageFile;
...@@ -516,7 +570,11 @@ class AddVideoCourse extends React.Component { ...@@ -516,7 +570,11 @@ class AddVideoCourse extends React.Component {
showCutModal, showSelectFileModal, diskList, showCutModal, showSelectFileModal, diskList,
imageFile, joinType, videoName, videoType,shelfState, imageFile, joinType, videoName, videoType,shelfState,
categoryName,courseCatalogList,whetherVisitorsJoin, categoryName,courseCatalogList,whetherVisitorsJoin,
visible,showSelectCoverModal,hasImgReady,cutImageBlob visible,showSelectCoverModal,hasImgReady,cutImageBlob,
introduce,
loadintroduce,
id,
showSelectImageModal,
} = this.state; } = this.state;
// 已选择的上课学员数量 // 已选择的上课学员数量
...@@ -605,10 +663,12 @@ class AddVideoCourse extends React.Component { ...@@ -605,10 +663,12 @@ class AddVideoCourse extends React.Component {
<div className="intro-info mt16"> <div className="intro-info mt16">
<AddVideoIntro <AddVideoIntro
data={{ data={{
id,
liveCourseMediaRequests: scheduleMedia, liveCourseMediaRequests: scheduleMedia,
shelfState, shelfState,
whetherVisitorsJoin, whetherVisitorsJoin,
label: '视频课简介' introduce,
loadintroduce,
}} }}
onChange={this.handleChangeForm} onChange={this.handleChangeForm}
/> />
...@@ -642,6 +702,21 @@ class AddVideoCourse extends React.Component { ...@@ -642,6 +702,21 @@ class AddVideoCourse extends React.Component {
onSelect={this.handleSelectVideo} onSelect={this.handleSelectVideo}
/> />
} }
{showSelectImageModal &&
<SelectPrepareFileModal
key="basic"
operateType="select"
multiple={false}
accept="image/jpeg,image/png,image/jpg"
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectImageModal}
onClose={() => {
this.setState({ showSelectImageModal: false })
}}
onSelect={this.handleSelectImage}
/>
}
{showSelectCoverModal && {showSelectCoverModal &&
<SelectPrepareFileModal <SelectPrepareFileModal
key="basic" key="basic"
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-07-16 11:05:17 * @Date: 2020-07-16 11:05:17
* @Last Modified by: mikey.zhaopeng * @Last Modified by: chenshu
* @Last Modified time: 2020-11-24 14:29:52 * @Last Modified time: 2021-03-23 18:12:05
* @Description: 添加直播-简介 * @Description: 添加直播-简介
*/ */
import React from 'react'; import React from 'react';
import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd'; import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd';
import Service from '@/common/js/service'; import Service from '@/common/js/service';
import EditorBox from '../../components/EditorBox'; import GraphicsEditor from '../../components/GraphicsEditor';
import User from '@/common/js/user'; import User from '@/common/js/user';
import UploadOss from '@/core/upload'; import UploadOss from '@/core/upload';
import './AddVideoIntro.less'; import './AddVideoIntro.less';
...@@ -144,6 +144,10 @@ class AddVideoIntro extends React.Component { ...@@ -144,6 +144,10 @@ class AddVideoIntro extends React.Component {
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests); this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
} }
changeIntro = (value) => {
this.props.onChange('introduce', value);
}
handleAddIntroText = () => { handleAddIntroText = () => {
const { liveCourseMediaRequests } = this.props.data; const { liveCourseMediaRequests } = this.props.data;
liveCourseMediaRequests.push({ liveCourseMediaRequests.push({
...@@ -180,7 +184,7 @@ class AddVideoIntro extends React.Component { ...@@ -180,7 +184,7 @@ class AddVideoIntro extends React.Component {
} }
render() { render() {
const {data: { whetherVisitorsJoin,liveCourseMediaRequests = [], shelfState} } = this.props; const {data: { whetherVisitorsJoin,liveCourseMediaRequests = [], shelfState, id, introduce, loadintroduce } } = this.props;
const {showSelectFileModal,selectType} = this.state const {showSelectFileModal,selectType} = this.state
return ( return (
<div className="add-video__intro-info"> <div className="add-video__intro-info">
...@@ -218,49 +222,19 @@ class AddVideoIntro extends React.Component { ...@@ -218,49 +222,19 @@ class AddVideoIntro extends React.Component {
<span className="label">视频课简介:</span> <span className="label">视频课简介:</span>
<div className="content"> <div className="content">
<div className="intro-list"> <div className="intro-list">
{ <div className="intro-list__item introduce-editor">
liveCourseMediaRequests.map((item, index) => { {(!id || loadintroduce) &&
if (item.mediaType === 'TEXT') { <GraphicsEditor
return ( id="intro"
<div className="intro-list__item" key={item.key}> isIntro={true}
<EditorBox
detail={{ detail={{
content: item.mediaContent content: introduce
}} }}
onChange={(val, length) => { this.handleChangeIntro(index, val, length) }} onChange={(val) => { this.changeIntro(val) }}
/> />
{this.renderLittleIcon(index)}
</div>
)
}
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-list__item picture" key={index}>
<div className="img__wrap">
<img src={item.mediaUrl} />
</div>
{this.renderLittleIcon(index)}
</div>
)
}
})
} }
</div> </div>
<div className="operate">
<div className="operate__item" onClick={this.handleAddIntroText}>
<span className="icon iconfont">&#xe639;</span>
<span className="text">文字</span>
</div> </div>
<div className="operate__item" onClick={this.handleUpload}>
<span className="icon iconfont">&#xe63b;</span>
<span className="text">图片</span>
</div>
</div>
<div className="tips">
• 图片支持jpeg、jpg、png、gif格式
</div>
</div> </div>
</div> </div>
{/* 选择暖场图文件弹窗 */} {/* 选择暖场图文件弹窗 */}
......
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-08-05 10:12:45 * @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan * @LastEditors: yuananting
* @LastEditTime: 2021-03-16 15:20:35 * @LastEditTime: 2021-03-24 19:34:48
* @Description: 视频课-列表模块 * @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有 * @Copyright: 杭州杰竞科技有限公司 版权所有
*/ */
...@@ -326,6 +326,7 @@ class VideoCourseList extends React.Component { ...@@ -326,6 +326,7 @@ class VideoCourseList extends React.Component {
needStr={needStr} needStr={needStr}
data={shareData} data={shareData}
type="videoClass" type="videoClass"
title="视频课"
close={() => { close={() => {
this.setState({ this.setState({
shareLiveModal: null shareLiveModal: null
......
...@@ -71,9 +71,11 @@ class Home extends React.Component { ...@@ -71,9 +71,11 @@ class Home extends React.Component {
incCustomerNum: res.result.incCustomerNum, incCustomerNum: res.result.incCustomerNum,
incLiveCourseNum: res.result.incLiveCourseNum, incLiveCourseNum: res.result.incLiveCourseNum,
incVideoCourseNum: res.result.incVideoCourseNum, incVideoCourseNum: res.result.incVideoCourseNum,
incPictureCourseNum: res.result.incPictureCourseNum,
liveCourseNum: res.result.liveCourseNum, liveCourseNum: res.result.liveCourseNum,
totalCustomerNum: res.result.totalCustomerNum, totalCustomerNum: res.result.totalCustomerNum,
videoCourseNum: res.result.videoCourseNum, videoCourseNum: res.result.videoCourseNum,
pictureCourseNum: res.result.pictureCourseNum,
}) })
} }
}) })
...@@ -182,9 +184,11 @@ class Home extends React.Component { ...@@ -182,9 +184,11 @@ class Home extends React.Component {
incCustomerNum, incCustomerNum,
incLiveCourseNum, incLiveCourseNum,
incVideoCourseNum, incVideoCourseNum,
incPictureCourseNum,
liveCourseNum, liveCourseNum,
totalCustomerNum, totalCustomerNum,
videoCourseNum, videoCourseNum,
pictureCourseNum,
timeRange, timeRange,
scheduleType, scheduleType,
studyTimeRange, studyTimeRange,
...@@ -212,7 +216,7 @@ class Home extends React.Component { ...@@ -212,7 +216,7 @@ class Home extends React.Component {
<img className="header-icon" src="https://image.xiaomaiketang.com/xm/jPrRhw8EMF.png" /> <img className="header-icon" src="https://image.xiaomaiketang.com/xm/jPrRhw8EMF.png" />
<span className="header-word">课程总数 (个)</span> <span className="header-word">课程总数 (个)</span>
</div> </div>
<div className="data-number">{videoCourseNum + liveCourseNum}</div> <div className="data-number">{videoCourseNum + liveCourseNum + pictureCourseNum}</div>
<div className="course-box"> <div className="course-box">
<div className="course-item"> <div className="course-item">
<div className="course-title">直播课</div> <div className="course-title">直播课</div>
...@@ -239,12 +243,12 @@ class Home extends React.Component { ...@@ -239,12 +243,12 @@ class Home extends React.Component {
<div className="course-item"> <div className="course-item">
<div className="course-title">图文课</div> <div className="course-title">图文课</div>
<div className="data"> <div className="data">
<span className="course-number">0</span> <span className="course-number">{pictureCourseNum}</span>
<span className="course-word">本月新增</span> <span className="course-word">本月新增</span>
{false && {incPictureCourseNum > 0 &&
<span className="icon iconfont">&#xe635;</span> <span className="icon iconfont">&#xe635;</span>
} }
<span className="add-number">0</span> <span className="add-number">{incPictureCourseNum}</span>
</div> </div>
</div> </div>
<div className="course-item"> <div className="course-item">
...@@ -303,6 +307,10 @@ class Home extends React.Component { ...@@ -303,6 +307,10 @@ class Home extends React.Component {
className={`tab${scheduleType === 'VOICE' ? ' selected' : ''}`} className={`tab${scheduleType === 'VOICE' ? ' selected' : ''}`}
onClick={() => this.setState({ scheduleType: 'VOICE' }, () => this.getHotCourse())} onClick={() => this.setState({ scheduleType: 'VOICE' }, () => this.getHotCourse())}
>视频课</span> >视频课</span>
<span
className={`tab${scheduleType === 'PICTURE' ? ' selected' : ''}`}
onClick={() => this.setState({ scheduleType: 'PICTURE' }, () => this.getHotCourse())}
>图文课</span>
</div> </div>
<div className="study-select"> <div className="study-select">
<span className="select-word">{moment().subtract(timeRange - 1, 'day').format('MM.DD')} ~ {moment().format('MM.DD')}</span> <span className="select-word">{moment().subtract(timeRange - 1, 'day').format('MM.DD')} ~ {moment().format('MM.DD')}</span>
......
...@@ -292,7 +292,7 @@ class BasicInfo extends React.Component{ ...@@ -292,7 +292,7 @@ class BasicInfo extends React.Component{
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col span={8}> <Col span={24}>
<Radio value="Assign_Operate"> <Radio value="Assign_Operate">
指定运营师 指定运营师
<span className="playback__text">仅被选择的运营师有权限可见</span> <span className="playback__text">仅被选择的运营师有权限可见</span>
...@@ -324,7 +324,7 @@ class BasicInfo extends React.Component{ ...@@ -324,7 +324,7 @@ class BasicInfo extends React.Component{
</span> </span>
</div> </div>
<div className="video-standard-info"> <div className="live-standard-info">
<span className="icon iconfont">&#xe864;</span> <span className="icon iconfont">&#xe864;</span>
<span className="instro">视频课单个课程,用户学习进度达到 <span className="instro">视频课单个课程,用户学习进度达到
<Input <Input
...@@ -337,6 +337,19 @@ class BasicInfo extends React.Component{ ...@@ -337,6 +337,19 @@ class BasicInfo extends React.Component{
%,即视为"已完成"学习 %,即视为"已完成"学习
</span> </span>
</div> </div>
{/* <div className="video-standard-info">
<span className="icon iconfont">&#xe864;</span>
<span>图文课单个课程,用户学习进度达到
<Input
width="40"
value={percentCompleteVideo}
onChange={(e) => { this.props.onChange('percentCompleteVideo', e.target.value.replace(/\D/g,'')) }}
onBlur={(e) => this.percentCompleteBlur(e, 'percentCompleteVideo')}
className="input-box"
/>
%,即视为“已完成”学习
</span>
</div> */}
</div> </div>
</div> </div>
{ operatorModalVisible && { operatorModalVisible &&
......
...@@ -526,6 +526,7 @@ class SelectPrepareFileModal extends React.Component { ...@@ -526,6 +526,7 @@ class SelectPrepareFileModal extends React.Component {
footer={footer} footer={footer}
width={680} width={680}
maskClosable={false} maskClosable={false}
destroyOnClose={true}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>} closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
onCancel={this.handleClose} onCancel={this.handleClose}
className="select-prepare-file-modal" className="select-prepare-file-modal"
......
...@@ -234,6 +234,7 @@ ...@@ -234,6 +234,7 @@
background-color: #fff; background-color: #fff;
flex-wrap: wrap; flex-wrap: wrap;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
z-index: 10;
.pc-url { .pc-url {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
......
/* /*
* @Author: wufan * @Author: wufan
* @Date: 2020-11-30 10:47:38 * @Date: 2020-11-30 10:47:38
* @LastEditors: zhangleyuan * @LastEditors: yuananting
* @LastEditTime: 2021-03-11 15:30:00 * @LastEditTime: 2021-03-18 11:29:43
* @Description: web店铺banner页面 * @Description: web店铺banner页面
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有 * @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/ */
......
/*
* @Author: yuananting
* @Date: 2021-02-25 13:46:35
* @LastEditors: yuananting
* @LastEditTime: 2021-03-25 18:25:34
* @Description: 助学工具-题库-题目管理-新增题目
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Tabs, Button, Tooltip, message, Modal } from "antd";
import Breadcrumbs from "@/components/Breadcrumbs";
import ShowTips from "@/components/ShowTips";
import "./AddNewQuestion.less";
import NewQuestionTab from "./components/NewQuestionTab";
import {
defineJudgeOptionInfo,
defineOptionInfo,
defineQuestionInfo,
} from "./components/model";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
import User from "@/common/js/user";
const { TabPane } = Tabs;
class AddNewQuestion extends Component {
constructor(props) {
super(props);
let activeKey = "";
if (getParameterByName("type")) {
activeKey = getParameterByName("type");
} else if (getParameterByName("key")) {
activeKey = getParameterByName("key");
} else {
activeKey = "SINGLE_CHOICE";
}
this.state = {
activeKey: activeKey,
// 构建题目基本结构
singleChoiceContent: defineQuestionInfo("SINGLE_CHOICE"), // 单选题
multiChoiceContent: defineQuestionInfo("MULTI_CHOICE"), // 多选题
judgeContent: defineQuestionInfo("JUDGE"), // 判断题
gapFillingContent: defineQuestionInfo("GAP_FILLING"), // 填空题
indefiniteChoiceContent: defineQuestionInfo("INDEFINITE_CHOICE"), // 不定项选择题
currentOperate: "new",
};
}
componentDidMount() {
if (getParameterByName("id")) {
// 编辑
this.setState({ currentOperate: "edit" });
this.queryQuestionDetails();
}
}
transferStemDocument = (txt) => {
const template = `<p class='content'>${txt}</p>`;
let doc = new DOMParser().parseFromString(template, "text/html");
let p = doc.querySelector(".content");
return p;
};
queryQuestionDetails = () => {
let query = {
id: getParameterByName("id"),
source: 0,
userId: User.getStoreUserId(),
tenantId: User.getStoreId(),
};
QuestionBankService.queryQuestionDetails(query).then((res) => {
const { result = [] } = res;
let stemContent = _.find(
result.questionStemList,
(contentItem) => contentItem.type === "RICH_TEXT"
);
const { questionTypeEnum } = result;
switch (questionTypeEnum) {
case "SINGLE_CHOICE":
this.setState({ singleChoiceContent: result });
break;
case "MULTI_CHOICE":
this.setState({ multiChoiceContent: result });
break;
case "JUDGE":
this.setState({ judgeContent: result });
break;
case "GAP_FILLING":
stemContent.content = stemContent.content
.split("")
.map((item) => {
if (item === "_") {
return `<input class="add-fill-line" disabled correctAnswerList="" id=${window.random_string(
16
)} value="填空"/>`;
}
return item;
})
.join("");
this.setState({ gapFillingContent: result });
break;
case "INDEFINITE_CHOICE":
this.setState({ indefiniteChoiceContent: result });
break;
}
});
};
handleRest = (type) => {
this.setState({ currentOperate: "add" });
switch (type) {
case "SINGLE_CHOICE":
let singleChoiceContent = defineQuestionInfo("SINGLE_CHOICE");
for (var i = 0; i < 4; i++) {
singleChoiceContent.optionList.push(defineOptionInfo());
}
this.setState({ singleChoiceContent });
break;
case "MULTI_CHOICE":
let multiChoiceContent = defineQuestionInfo("MULTI_CHOICE");
for (var i = 0; i < 4; i++) {
multiChoiceContent.optionList.push(defineOptionInfo());
}
this.setState({ multiChoiceContent });
break;
case "JUDGE":
let judgeContent = defineQuestionInfo("JUDGE");
var judgeOptions = ["正确", "错误"];
judgeOptions.forEach((item) => {
judgeContent.optionList.push(defineJudgeOptionInfo(item));
});
this.setState({ judgeContent });
break;
case "GAP_FILLING":
this.setState({
gapFillingContent: defineQuestionInfo("GAP_FILLING"),
});
break;
case "INDEFINITE_CHOICE":
let indefiniteChoiceContent = defineQuestionInfo("INDEFINITE_CHOICE");
for (var i = 0; i < 4; i++) {
indefiniteChoiceContent.optionList.push(defineOptionInfo());
}
this.setState({ indefiniteChoiceContent });
}
};
initOption = (content) => {
chooseOptions.push(defineJudgeOptionInfo(content));
};
saveCurrentQuestion = (content, type, next) => {
content.questionStemList.map((item, index) => {
item.sort = index;
return item;
});
content.optionList.map((item) => {
item.questionOptionContentList.map((childItem, childIndex) => {
childItem.sort = childIndex;
return childItem;
});
return item;
});
content.questionAnswerDescList.map((item, index) => {
item.sort = index;
return item;
});
let params = {};
let categoryId = getParameterByName("categoryId");
if (getParameterByName("id") && this.state.currentOperate === "edit") {
params = {
...content,
id: getParameterByName("id"),
categoryId: categoryId || null,
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
QuestionBankService.editQuestion(params).then((res) => {
if (res.success) {
message.success("保存成功");
if (next === "add") {
this.handleRest(type);
}
if (next === "close") {
window.RCHistory.push({
pathname: `/question-bank-index?categoryId=${params.categoryId}`,
});
}
}
});
} else {
params = {
...content,
categoryId: getParameterByName("categoryId"),
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
QuestionBankService.addQuestion(params).then((res) => {
if (res.success) {
message.success("保存成功");
if (next === "add") {
this.handleRest(type);
}
if (next === "close") {
window.RCHistory.push({
pathname: `/question-bank-index?categoryId=${params.categoryId}`,
});
}
}
});
}
};
// 取消编辑并返回上一级路由
handleGoBack = () => {
Modal.confirm({
title: "确定要返回吗?",
content: "返回后,本次编辑的内容将不被保存",
okText: "确认返回",
cancelText: "留在本页",
icon: (
<span className="icon iconfont default-confirm-icon">&#xe6f4;</span>
),
onOk: () => {
window.RCHistory.goBack();
},
});
};
confirmSaveQuestion = (next) => {
const {
singleChoiceContent,
multiChoiceContent,
judgeContent,
gapFillingContent,
indefiniteChoiceContent,
} = this.state;
switch (this.state.activeKey) {
case "SINGLE_CHOICE":
if (this.singleChoiceRef.checkInput() === 0) {
this.saveCurrentQuestion(singleChoiceContent, "SINGLE_CHOICE", next);
}
break;
case "MULTI_CHOICE":
if (this.multiChoiceRef.checkInput() === 0) {
this.saveCurrentQuestion(multiChoiceContent, "MULTI_CHOICE", next);
}
break;
case "JUDGE":
if (this.judgeRef.checkInput() === 0) {
this.saveCurrentQuestion(judgeContent, "JUDGE", next);
}
break;
case "GAP_FILLING":
if (this.gapRef.checkInput() === 0) {
this.saveCurrentQuestion(gapFillingContent, "GAP_FILLING", next);
}
break;
case "INDEFINITE_CHOICE":
if (this.indefiniteRef.checkInput() === 0) {
this.saveCurrentQuestion(
indefiniteChoiceContent,
"INDEFINITE_CHOICE",
next
);
}
break;
}
};
handleLogger = (en, cn) => {
const { onLogger } = this.props;
onLogger && onLogger(en, cn);
};
render() {
const {
activeKey,
singleChoiceContent,
multiChoiceContent,
judgeContent,
gapFillingContent,
indefiniteChoiceContent,
} = this.state;
const categoryId = getParameterByName("categoryId");
return (
<div className="page add-new-question">
<Breadcrumbs
navList={
getParameterByName("id") && this.state.currentOperate === "edit"
? "编辑题目"
: "新增题目"
}
goBack={() => this.handleGoBack()}
/>
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企培保有依据国家规定及平台规则进行处理的权利" />
</div>
<Tabs
style={{ marginTop: 32 }}
activeKey={activeKey}
onChange={(activeKey) => {
this.setState({ activeKey });
}}
>
<TabPane
tab={
<span>
<span className="icon iconfont" style={{ color: activeKey === "SINGLE_CHOICE" ? "#ffb714" : "#CCCCCC" }}>
&#xe7fa;{" "}
</span>
<span>单选题</span>
</span>
}
key="SINGLE_CHOICE"
>
<NewQuestionTab
questionTypeKey={activeKey}
onRef={(ref) => {
this.singleChoiceRef = ref;
}}
questionInfo={singleChoiceContent}
onSetState={(newContent) => {
Object.assign(singleChoiceContent, newContent);
}}
onLogger={this.handleLogger}
/>
</TabPane>
<TabPane
tab={
<span>
<span className="icon iconfont" style={{ color: activeKey === "MULTI_CHOICE" ? "#ffb714" : "#CCCCCC" }}>
&#xe7fb;{" "}
</span>
<span>多选题</span>
</span>
}
key="MULTI_CHOICE"
>
<NewQuestionTab
questionTypeKey={activeKey}
onRef={(ref) => {
this.multiChoiceRef = ref;
}}
questionInfo={multiChoiceContent}
onSetState={(newContent) => {
Object.assign(multiChoiceContent, newContent);
}}
onLogger={this.handleLogger}
/>
</TabPane>
<TabPane
tab={
<span>
<span className="icon iconfont" style={{ color: activeKey === "JUDGE" ? "#ffb714" : "#CCCCCC" }}>
&#xe7fc;{" "}
</span>
<span>判断题</span>
</span>
}
key="JUDGE"
>
<NewQuestionTab
questionTypeKey={activeKey}
onRef={(ref) => {
this.judgeRef = ref;
}}
questionInfo={judgeContent}
onSetState={(newContent) => {
Object.assign(judgeContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={
<span>
<span className="icon iconfont" style={{ color: activeKey === "GAP_FILLING" ? "#ffb714" : "#CCCCCC" }}>
&#xe7fd;{" "}
</span>
<span>填空题</span>
</span>
}
key="GAP_FILLING"
>
<NewQuestionTab
questionTypeKey={activeKey}
onRef={(ref) => {
this.gapRef = ref;
}}
questionInfo={gapFillingContent}
onSetState={(newContent) => {
Object.assign(gapFillingContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={
<span>
<span className="icon iconfont" style={{ color: activeKey === "INDEFINITE_CHOICE" ? "#ffb714" : "#CCCCCC" }}>
&#xe7fe;{" "}
</span>
<span>不定项选择题 </span>
<Tooltip title="至少有一项正确,至多不限的选择题,多项选择题的一种特殊形式">
<span className="icon iconfont" style={{ color: "#BFBFBF" }}>&#xe7c4;</span>
</Tooltip>
</span>
}
key="INDEFINITE_CHOICE"
>
<NewQuestionTab
questionTypeKey={activeKey}
onRef={(ref) => {
this.indefiniteRef = ref;
}}
questionInfo={indefiniteChoiceContent}
onSetState={(newContent) => {
Object.assign(indefiniteChoiceContent, newContent);
}}
onLogger={this.handleLogger}
/>
</TabPane>
</Tabs>
</div>
<div className="footer">
<Button
onClick={() => {
this.handleGoBack();
}}
>
取消
</Button>
{categoryId && categoryId !== "null" && (
<Button
onClick={() => {
this.confirmSaveQuestion("add");
}}
>
保存并继续添加
</Button>
)}
<Button
type="primary"
onClick={() => {
this.confirmSaveQuestion("close");
}}
>
保存
</Button>
</div>
</div>
);
}
}
export default AddNewQuestion;
/*
* @Author: yuananting
* @Date: 2021-02-25 13:52:01
* @LastEditors: yuananting
* @LastEditTime: 2021-03-18 09:32:11
* @Description: 助学工具-题库-题目管理-新增题目样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.add-new-question {
position: relative !important;
.box {
margin-bottom: 66px !important;
.ant-tabs {
color: #666666;
}
}
.footer {
position: fixed;
bottom: 0;
height: 58px;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 252px;
background: #fff;
border-top: 1px solid #e8e8e8;
z-index: 999;
.ant-btn {
margin-left: 10px;
}
}
}
/*
* @Author: yuananting
* @Date: 2021-02-21 17:51:01
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 13:55:56
* @Description: 助学工具-题库-题库主页面
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import "./QuestionBankIndex.less";
import QuestionBankSider from "./components/QuestionBankSider";
import QuestionManageContent from "./components/QuestionManageContent";
class QuestionBankIndex extends Component {
constructor(props) {
super(props);
this.state = {
selectedCategoryId: "",
};
}
componentDidMount() {}
getCategoryIdFromSider = (selectedCategoryId) => {
if (selectedCategoryId && selectedCategoryId.length > 0) {
this.setState({ selectedCategoryId: selectedCategoryId[0] });
}
};
updatedSiderTreeFromList = (currentTotal, updatedCategoryId) => {
this.setState({ currentTotal });
this.setState({ updatedCategoryId });
};
render() {
return (
<div className="question-bank-index page">
<div className="content-header">题目</div>
<div className="box content-body">
<div style={{borderRight: "0.5px solid #EEEEEE", paddingRight: "4px"}}>
<div className="sider">
<QuestionBankSider
getSelectedCategoryId={this.getCategoryIdFromSider.bind(this)}
currentTotal={this.state.currentTotal}
updatedCategoryId={this.state.updatedCategoryId}
/>
</div>
</div>
<div className="content">
<QuestionManageContent
updatedSiderTree={this.updatedSiderTreeFromList.bind(this)}
selectedCategoryId={this.state.selectedCategoryId}
/>
</div>
</div>
</div>
);
}
}
export default QuestionBankIndex;
/*
* @Author: yuananting
* @Date: 2021-02-21 18:27:43
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 16:15:03
* @Description: 助学工具-题库-题库主页面样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-bank-index {
.content-body {
display: flex;
.site-layout-background {
background: #fff;
}
.sider {
min-width: 244px;
}
.content {
width: 100%;
margin-left: 24px;
height: calc(100vh - 160px);
}
}
}
/*
* @Author: yuananting
* @Date: 2021-02-23 18:28:50
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 17:18:20
* @Description: 助学工具-题库-主页面分类管理
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import Breadcrumbs from "@/components/Breadcrumbs";
import "./QuestionCategoryManage.less";
import NewEditQuestionBankCategory from "./modal/NewEditQuestionBankCategory";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
import User from "@/common/js/user";
import {
Tree,
Input,
Space,
Button,
Menu,
Dropdown,
message,
Modal,
} from "antd";
import ShowTips from "@/components/ShowTips";
const { DirectoryTree } = Tree;
const { Search } = Input;
const { confirm } = Modal;
class QuestionCategoryManage extends Component {
constructor(props) {
super(props);
this.state = {
NewEditQuestionBankCategory: null, //新增或编辑分类模态框
treeData: [],
treeMap: {},
selectedKeys: ["0"],
autoExpandParent: true,
};
}
componentDidMount() {
this.queryCategoryTree("search");
}
// 查询分类树
queryCategoryTree = (operateType, categoryName) => {
this.setState({ categoryName });
let query = {
source: 0,
categoryName,
userId: User.getStoreUserId(),
tenantId: User.getStoreId(),
};
QuestionBankService.queryCategoryTree(query).then((res) => {
const { result = [] } = res;
let str = "未分类";
if (categoryName) {
this.setState({ autoExpandParent: true });
if (str.indexOf(categoryName) < 0) {
this.setState({
treeData: this.renderTreeNodes(result, categoryName),
});
let nodeId = [];
Object.keys(this.state.treeMap).forEach((item) => {
nodeId.push(item);
});
this.setState({ expandedKeys: nodeId });
} else {
const defaultNode = {
id: "0",
categoryName: "未分类",
categoryCount: 0,
parentId: "0",
categoryLevel: 0,
};
result.unshift(defaultNode);
this.setState({
treeData: this.renderTreeNodes(result, categoryName),
});
let nodeId = [];
Object.keys(this.state.treeMap).forEach((item) => {
nodeId.push(item);
});
if (operateType === "search") {
this.setState({ expandedKeys: nodeId });
}
}
} else {
this.setState({ autoExpandParent: false });
const defaultNode = {
id: "0",
categoryName: "未分类",
categoryCount: 0,
parentId: "0",
categoryLevel: 0,
};
result.unshift(defaultNode);
this.setState({
treeData: this.renderTreeNodes(result, categoryName),
});
if (operateType === "search") {
this.setState({ expandedKeys: [] });
}
}
});
};
// 删除分类
delCategory = (item) => {
return confirm({
title: "确认删除该分类吗?",
content: "删除后,分类下的所有内容将自动转入“未分类”中。",
icon: (
<span className="icon iconfont default-confirm-icon">&#xe839; </span>
),
okText: "删除",
okType: "danger",
cancelText: "取消",
onOk: () => {
let params = {
categoryId: item.id,
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
QuestionBankService.delCategory(params).then((res) => {
if (res.success) {
message.success("删除分类成功");
this.queryCategoryTree("change");
}
});
},
});
};
// 新增或编辑分类
newEditQuestionCategory = (categoryType, addLevelType, type, node) => {
let title = "";
let label = "";
switch (categoryType) {
case "newEqualLevelCategory":
title = "新增分类";
label = "分类名称";
break;
case "newChildLevelCategory":
title = "新增子分类";
label = "子分类名称";
break;
case "editEqualLevelCategory":
title = "编辑分类";
label = "分类名称";
break;
case "editChildLevelCategory":
title = "编辑子分类";
label = "子分类名称";
break;
}
const m = (
<NewEditQuestionBankCategory
node={node}
addLevelType={addLevelType}
type={type}
treeData={this.state.treeData}
title={title}
label={label}
close={() => {
this.queryCategoryTree("change");
this.setState({
NewEditQuestionBankCategory: null,
});
}}
/>
);
this.setState({ NewEditQuestionBankCategory: m });
};
initDropMenu = (item) => {
return (
<Menu>
<Menu.Item key="0">
<span
onClick={() => {
let categoryType =
item.categoryLevel === 0
? "editEqualLevelCategory"
: "editChildLevelCategory";
this.newEditQuestionCategory(categoryType, "equal", "edit", item);
}}
>
重命名
</span>
</Menu.Item>
<Menu.Item key="1">
<span
onClick={() => {
this.delCategory(item);
}}
>
删除
</span>
</Menu.Item>
</Menu>
);
};
getRelatedNodes = (parentId) => {
return this.state.treeMap[parentId]
? this.state.treeMap[parentId].sonCategoryList
: [];
};
getParentDragNodesLevel = (dragNode) => {
if (!dragNode) {
return [];
}
let dragNodes = [];
dragNodes.push(dragNode.id);
if (dragNode.parentId != 0) {
dragNodes = dragNodes.concat(
this.getParentDragNodesLevel(this.state.treeMap[dragNode.parentId])
);
}
return dragNodes;
};
getDragNodesLevel = (dragNode) => {
let dragNodes = [];
if (dragNode.sonCategoryList && dragNode.sonCategoryList.length > 0) {
dragNode.sonCategoryList.forEach((item) => {
dragNodes.push(item.categoryLevel);
if (item.sonCategoryList && item.sonCategoryList.length > 0) {
dragNodes = dragNodes.concat(this.getDragNodesLevel(item));
}
});
}
return [...new Set(dragNodes)];
};
onDrop = (info) => {
if (this.state.categoryName) {
return;
}
// 未分类不可以拖拽
if (
info.dragNode.categoryName === "未分类" &&
info.dragNode.categoryLevel === 0
)
return message.info("未分类”为默认分类暂不支持移动");
// 不允许其他节点拖拽到未分类之前
if (
info.node.categoryName === "未分类" &&
info.dropToGap &&
info.dropPosition === -1
)
return;
let targetParentId = info.dropToGap ? info.node.parentId : info.node.id;
if (this.state.treeMap[targetParentId].categoryLevel === 4) {
return message.info("最多支持5级分类");
} else {
let nodesArr = this.getDragNodesLevel(
this.state.treeMap[info.dragNode.id]
);
let parentArr = this.getParentDragNodesLevel(
this.state.treeMap[targetParentId]
);
if (nodesArr.length + parentArr.length > 4) {
return message.info("最多支持5级分类");
}
}
let relatedNodes = this.getRelatedNodes(targetParentId);
if (relatedNodes && relatedNodes.length === 30) {
return message.info("最多只能添加30个子分类");
}
const dropKey = info.node.key;
const dragKey = info.dragNode.key;
const dropPos = info.node.pos.split("-");
const dropPosition =
info.dropPosition - Number(dropPos[dropPos.length - 1]);
const loop = (data, key, callback) => {
for (let i = 0; i < data.length; i++) {
if (data[i].key === key) {
return callback(data[i], i, data);
}
if (data[i].sonCategoryList) {
loop(data[i].sonCategoryList, key, callback);
}
}
};
const data = [...this.state.treeData];
let getSuf = function (name, originCategoryName, sufIndex) {
if (relatedNodes && relatedNodes.length > 0) {
let sameNameNodes = [];
relatedNodes.forEach((item) => {
if (item.id === info.dragNode.id) return true;
if (item.categoryName === name) {
sameNameNodes.push(item);
}
});
if (sameNameNodes.length > 0) {
sufIndex++;
return getSuf(
originCategoryName + `(${sufIndex})`,
originCategoryName,
sufIndex
);
}
}
return sufIndex;
};
let dragObj;
loop(data, dragKey, (item, index, arr) => {
arr.splice(index, 1);
item.parentId = targetParentId;
if (item.originCategoryName) {
item.categoryName = item.originCategoryName;
} else {
item.originCategoryName = item.categoryName;
}
info.dragNode.categoryName = item.originCategoryName;
let sufIndex = getSuf(
info.dragNode.categoryName,
item.originCategoryName,
0
);
item.categoryName = item.categoryName + (sufIndex ? `(${sufIndex})` : "");
item.categoryName =
item.originCategoryName + (sufIndex ? `(${sufIndex})` : "");
dragObj = item;
});
if (!info.dropToGap) {
loop(data, dropKey, (item) => {
item.sonCategoryList = item.sonCategoryList || [];
item.sonCategoryList.unshift(dragObj);
});
} else if (
(info.node.props.sonCategoryList || []).length > 0 &&
info.node.props.expanded &&
dropPosition === 1
) {
loop(data, dropKey, (item) => {
item.sonCategoryList = item.children || [];
item.sonCategoryList.unshift(dragObj);
});
} else {
let ar;
let i;
loop(data, dropKey, (item, index, arr) => {
ar = arr;
i = index;
});
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
} else {
ar.splice(i + 1, 0, dragObj);
}
}
data.shift();
let newTreeData = this.renderTreeNodes(this.handleLoop(data, 0));
this.setState({ treeData: newTreeData });
let params = {
categoryList: newTreeData,
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
QuestionBankService.editCategoryTree(params).then((res) => {
this.queryCategoryTree("change");
});
};
handleLoop = (data, level) => {
data.map((item, index) => {
item.sort = index;
item.categoryLevel = level;
if (item.sonCategoryList) {
item.children = this.handleLoop(item.sonCategoryList, level + 1);
item.sonCategoryList = this.handleLoop(item.sonCategoryList, level + 1);
}
return item;
});
return data;
};
/** 获取树状第一级key 设置默认展开第一项 */
getFirstLevelKeys = (data) => {
let firstLevelKeys = [];
data.forEach((item) => {
if (item.categoryLevel === 0) {
firstLevelKeys.push(item.key);
}
});
return firstLevelKeys;
};
/** 树状展开事件 */
onExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
renderTreeNodes = (data, value) => {
let newTreeData = data.map((item) => {
item.title = item.categoryName;
item.key = item.id;
item.title = (
<div
style={{
opacity:
!value || (value && item.categoryName.indexOf(value) > -1)
? 1
: 0.5,
}}
className="node-title-div"
onMouseOver={(e) => {
let mouseNodeOpts = e.currentTarget.getElementsByTagName("div")[0];
if (mouseNodeOpts) {
mouseNodeOpts.style.visibility = "visible";
}
}}
onMouseOut={(e) => {
let mouseNodeOpts = e.currentTarget.getElementsByTagName("div")[0];
if (mouseNodeOpts) {
mouseNodeOpts.style.visibility = "hidden";
}
}}
>
<span>{item.categoryName}</span>
{item.categoryName !== "未分类" && (
<Space className="title-opts" size={16}>
<span
onClick={() => {
let nodesCount = 0;
const { treeData } = this.state;
if (item.categoryLevel === 0) {
// 第一层级
nodesCount = treeData.length;
} else {
let parentNodes = this.getRelatedNodes(item.parentId);
if (
parentNodes.length > 0 &&
parentNodes[0].sonCategoryList
) {
nodesCount = parentNodes[0].sonCategoryList.length;
} else {
nodesCount = 0;
}
}
if (nodesCount >= 30) {
message.info("最多只能添加30个分类");
return;
}
this.newEditQuestionCategory(
"newEqualLevelCategory",
"equal",
"new",
item
);
}}
>
<span className="icon iconfont" style={{ color: "#BFBFBF" }}>
&#xe7f5;{" "}
</span>
<span>同级</span>
</span>
{item.categoryLevel < 4 && (
<span
onClick={() => {
if (
item.sonCategoryList &&
item.sonCategoryList.length >= 30
) {
message.info("最多只能添加30个子分类");
return;
}
this.newEditQuestionCategory(
"newChildLevelCategory",
"child",
"new",
item
);
}}
>
<span className="icon iconfont" style={{ color: "#BFBFBF" }}>
&#xe7f8;{" "}
</span>
<span>子级</span>
</span>
)}
<Dropdown overlay={this.initDropMenu(item)}>
<span>
<span className="icon iconfont" style={{ color: "#BFBFBF" }}>
&#xe7f7;{" "}
</span>
<span>更多</span>
</span>
</Dropdown>
</Space>
)}
</div>
);
item.icon =
item.categoryName === "未分类" ? (
<img
style={{
width: "24px",
height: "24px",
opacity:
!value || (value && item.categoryName.indexOf(value) > -1)
? 1
: 0.5,
}}
src="https://image.xiaomaiketang.com/xm/defaultCategory.png"
alt=""
/>
) : (
<img
style={{
width: "24px",
height: "24px",
opacity:
!value || (value && item.categoryName.indexOf(value) > -1)
? 1
: 0.5,
}}
src="https://image.xiaomaiketang.com/xm/hasCategory.png"
alt=""
/>
);
if (item.sonCategoryList) {
item.children = this.renderTreeNodes(item.sonCategoryList, value);
}
return item;
});
let map = {};
let topItem = [];
data.forEach((item) => {
topItem.push(item);
});
this.setState({
treeMap: Object.assign(this.getTreeMap(data, map), {
0: {
sonCategoryList: topItem,
},
}),
});
return newTreeData;
};
getTreeMap = (data, map) => {
data.forEach((item) => {
map[item.id] = item;
if (item.sonCategoryList && item.sonCategoryList.length > 0) {
this.getTreeMap(item.sonCategoryList, map);
}
});
return map;
};
/** 树状选中事件 */
onSelect = (selectedKeys) => {
this.setState({ selectedKeys });
};
render() {
const {
treeData,
expandedKeys,
selectedKeys,
autoExpandParent,
} = this.state;
return (
<div className="page question-category-manage">
{getParameterByName("from") === "aid" ? (
<Breadcrumbs
navList="课程分类"
goBack={() =>
window.RCHistory.push({
pathname: "/question-bank-index",
})
}
/>
) : (
<div className="content-header">课程分类</div>
)}
<div className="box">
<div className="search-condition">
<span className="search-label">搜索名称:</span>
<Search
placeholder="请输入名称"
style={{ width: "300px" }}
onSearch={(value) => this.queryCategoryTree("search", value)}
className="search-input"
enterButton={<span className="icon iconfont">&#xe832;</span>}
/>
</div>
<Button
type="primary"
onClick={() => {
if (treeData.length >= 30) {
message.info("最多只能添加30个分类");
return;
}
this.newEditQuestionCategory(
"newEqualLevelCategory",
"equal",
"new"
);
}}
>
新增一级分类
</Button>
<div
className="show-tips"
style={{ marginTop: "12px", width: "900px" }}
>
<ShowTips message="为方便管理,该分类用于课程、培训计划、题库、知识库等模块,改动将同步各模块更新" />
</div>
<div className="course-category-tree">
<DirectoryTree
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={this.onExpand}
selectedKeys={selectedKeys}
onSelect={this.onSelect}
draggable
blockNode
onDrop={this.onDrop}
treeData={treeData}
></DirectoryTree>
</div>
</div>
{this.state.NewEditQuestionBankCategory}
</div>
);
}
}
export default QuestionCategoryManage;
/*
* @Author: yuananting
* @Date: 2021-02-23 19:41:42
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 17:16:43
* @Description: 助学工具-题库-题目分类管理样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-category-manage {
position: relative;
.search-condition {
width: 30%;
margin-bottom: 16px;
.search-label {
vertical-align: middle;
display: inline-block;
height: 32px;
line-height: 32px;
}
}
.course-category-tree {
position: relative;
margin-top: 16px;
width: 900px;
border: 1px solid #E8E8E8;
.ant-tree.ant-tree-directory {
font-size: 14px;
font-weight: 400;
color: #666666;
.anticon {
color: #999999;
}
.ant-tree-treenode {
height: 44px;
padding: 0;
span {
line-height: 44px;
vertical-align: middle;
}
.ant-tree-node-content-wrapper.ant-tree-node-selected {
color: #666666;
}
.ant-tree-node-content-wrapper {
display: flex;
.ant-tree-title {
width: 100%;
display: flex;
*.node-title-div {
width: 100%;
display: flex;
justify-content: space-between;
.title-opts {
visibility: hidden;
margin-right: 16px;
}
}
}
}
}
.ant-tree-treenode-selected:hover::before,
.ant-tree-treenode-selected::before {
background: #fffbf1;
}
}
}
.xm-show-tip {
background: #f1f3f6 !important;
span.icon {
color: #bfbfbf !important;
}
}
.ant-tree .ant-tree-node-content-wrapper .ant-tree-iconEle {
line-height: 37px !important;
margin-right: 8px;
}
.ant-tree.ant-tree-directory .ant-tree-treenode:hover::before {
background-color: #F3F6FA;
}
}
/*
* @Author: yuananting
* @Date: 2021-02-25 14:34:29
* @LastEditors: yuananting
* @LastEditTime: 2021-03-25 19:52:09
* @Description: 助学工具-题库-题目管理-新建题目Tab
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import {
Form,
Radio,
message,
Checkbox,
Tag,
Modal,
Input,
Popover,
} from "antd";
import "./NewQuestionTab.less";
import QuestionEditor from "./QuestionEditor";
import { PlusOutlined, CloseOutlined } from "@ant-design/icons";
import {
NUM_TO_WORD_MAP,
MEDIA_FILE_ACCEPT,
} from "@/common/constants/punchClock/punchClock";
import { defineOptionInfo, defineJudgeOptionInfo } from "./model";
import XMAudio from "./XMAudio";
import XMRecord from "./XMRecord";
import ScanFileModal from "@/modules/resource-disk/modal/ScanFileModal";
import SelectPrepareFileModal from "@/modules/prepare-lesson/modal/SelectPrepareFileModal";
import _ from "lodash";
class NewQuestionTab extends Component {
constructor(props) {
super(props);
const { questionInfo = {} } = props;
const {
questionStemList,
gapFillingAnswerList,
optionList,
questionAnswerDescList,
showBox,
} = questionInfo;
this.state = {
stemContent: JSON.parse(JSON.stringify(questionStemList)), // 题干内容
gapFillingAnswer: JSON.parse(JSON.stringify(gapFillingAnswerList)), // 填空题-选项列表
chooseOptions: JSON.parse(JSON.stringify(optionList)), // 单选多选不定项判断-选项列表
questionAnswerDesc: JSON.parse(JSON.stringify(questionAnswerDescList)), // 答案解析
accept: MEDIA_FILE_ACCEPT["PICTURE"], // 上传媒体类型
fileType: "PICTURE", // 媒体枚举
showRecord: false, // 录音弹窗
showBox: showBox,
gapFillingOptions: [],
blanksList: [], // 填空列表
showSelectFileModal: false,
mediaType: "",
diskList: [], // 资料云盘文件列表
};
this.uploadInput = React.createRef();
this.markKey = window.random_string(16);
}
componentDidMount() {
const { chooseOptions } = this.state;
if (
["INDEFINITE_CHOICE", "MULTI_CHOICE", "SINGLE_CHOICE"].includes(
this.props.questionTypeKey
)
) {
if (chooseOptions.length === 0) {
// 选择题(单选 多选 不定项)-插入4条默认选项
for (var i = 0; i < 4; i++) {
this.handleAddOption();
this.setState({ [`optionsValidate_${i}`]: "success" });
this.setState({ [`optionsText_${i}`]: "" });
}
}
} else if (this.props.questionTypeKey === "JUDGE") {
this.initJudgeOption("正确");
this.initJudgeOption("错误");
}
this.props.onRef(this);
}
handleLogger = (en, cn) => {
const { onLogger } = this.props;
onLogger && onLogger(en, cn);
};
static getDerivedStateFromProps(nextProps, prevState) {
// 控制录音组件展示
if (nextProps.showBox && !prevState.showBox) {
return {
showRecord: false,
showBox: nextProps.showBox,
};
}
return {
showBox: nextProps.showBox,
};
}
shouldComponentUpdate(nextProps, nextState) {
const { questionInfo } = nextProps;
if (this.props.questionInfo !== questionInfo) {
this.setState({
gapFillingAnswer: JSON.parse(
JSON.stringify(questionInfo.gapFillingAnswerList)
),
});
this.setState(
{
stemContent: JSON.parse(
JSON.stringify(questionInfo.questionStemList)
),
},
() => {
const con = questionInfo.questionStemList[0].content;
const input = con.match(/<input([^<>]*)>/g);
let _blanksList =
input &&
input.map((item) => {
return this.transferStemDocument(item).firstChild;
});
this.setState({ blanksList: _blanksList || [] });
}
); // 题干内容
this.setState({
chooseOptions: JSON.parse(JSON.stringify(questionInfo.optionList)),
}); // 单选多选不定项-选项列表
this.setState({
questionAnswerDesc: JSON.parse(
JSON.stringify(questionInfo.questionAnswerDescList)
),
}); // 答案解析
this._onSetState();
}
return true;
}
_onSetState = (params = {}) => {
this.setState({ ...params, updateKey: window.random_string(16) }, () => {
this.props.onSetState({
questionStemList: JSON.parse(JSON.stringify(this.state.stemContent)),
gapFillingAnswerList: JSON.parse(
JSON.stringify(this.state.gapFillingAnswer)
),
optionList: JSON.parse(JSON.stringify(this.state.chooseOptions)),
questionAnswerDescList: JSON.parse(
JSON.stringify(this.state.questionAnswerDesc)
),
});
});
};
transferStemDocument = (txt) => {
const template = `<div class='stem'>${txt}</div>`;
let doc = new DOMParser().parseFromString(template, "text/html");
let div = doc.querySelector(".stem");
return div;
};
// 保存校验
checkInput = () => {
let validateError = 0;
// 题干校验
let stemContent = _.find(
this.state.stemContent,
(contentItem) => contentItem.type === "RICH_TEXT"
);
let stem = stemContent.content.replace(/<[^>]+>/g, "");
stem = stem.replace(/\&nbsp\;/gi, "");
stem = stem.replace(/\s+/g, "");
if (this.props.questionTypeKey === "GAP_FILLING") {
if (this.state.blanksList.length === 0 || stem.length === 0) {
this.setState({ stemValidate: "error" });
this.setState({
stemText: (
<div style={{ marginTop: 8, minWidth: "523px" }}>
请输入正确格式,示例:党章规定,凡事有
<span style={{ padding: "0 10px", borderBottom: "1px solid" }}>
填空1
</span>
人以上的
<span style={{ padding: "0 10px", borderBottom: "1px solid" }}>
填空2
</span>
,都应该成立党的基层组织
</div>
),
});
validateError++;
} else {
this.setState({ stemValidate: "success" });
this.setState({ stemText: "" });
}
} else {
if (stem.length === 0) {
this.setState({ stemValidate: "error" });
this.setState({ stemText: "请输入题干" });
validateError++;
} else {
this.setState({ stemValidate: "success" });
this.setState({ stemText: "" });
}
}
// 选项校验
let optionUnChecked = 0;
const { chooseOptions } = this.state;
if (this.props.questionTypeKey === "GAP_FILLING") {
this.state.gapFillingAnswer.forEach((item, index) => {
if (item.correctAnswerList.length === 0) {
this.setState({ [`optionsValidate_${index}`]: "error" });
this.setState({ [`optionsText_${index}`]: "请输入答案" });
validateError++;
} else {
this.setState({ [`optionsValidate_${index}`]: "success" });
this.setState({ [`optionsText_${index}`]: "" });
}
});
} else {
chooseOptions.forEach((item, index) => {
const optionContent = item.questionOptionContentList;
optionUnChecked = item.isCorrectAnswer
? optionUnChecked
: optionUnChecked + 1;
let optionInput = optionContent[0].content.replace(/<[^>]+>/g, "");
optionInput = optionInput.replace(/\&nbsp\;/gi, "");
optionInput = optionInput.replace(/\s+/g, "");
if (
optionContent.length === 1 &&
optionContent[0].type === "RICH_TEXT" &&
optionInput.length === 0
) {
this.setState({ [`optionsValidate_${index}`]: "error" });
this.setState({ [`optionsText_${index}`]: "请输入选项" });
validateError++;
} else {
this.setState({ [`optionsValidate_${index}`]: "success" });
this.setState({ [`optionsText_${index}`]: "" });
}
});
var chooseIcon = [];
if (["SINGLE_CHOICE", "JUDGE"].includes(this.props.questionTypeKey)) {
chooseIcon = document.getElementsByClassName("ant-radio-inner");
} else if (
["MULTI_CHOICE", "INDEFINITE_CHOICE"].includes(
this.props.questionTypeKey
)
) {
chooseIcon = document.getElementsByClassName("ant-checkbox-inner");
}
if (optionUnChecked === chooseOptions.length) {
this.setState({ radioValidate: "error" });
chooseIcon.forEach((item) => {
item.setAttribute("style", "border:1px solid #ff4d4f;");
});
this.setState({
radioText: (
<span>
正确答案
<br />
不能为空
</span>
),
});
validateError++;
} else {
this.setState({ radioValidate: "success" });
this.setState({ radioText: "" });
chooseIcon.forEach((item) => {
item.removeAttribute("style");
});
}
if (
this.props.questionTypeKey === "MULTI_CHOICE" &&
this.state.chooseOptions.length - optionUnChecked === 1
) {
this.setState({ radioValidate: "error" });
this.setState({ radioText: "最少选两个" });
chooseIcon.forEach((item) => {
item.setAttribute("style", "border:1px solid #ff4d4f;");
});
validateError++;
}
}
return validateError;
};
/**
* 预览
*
* @memberof QuestionInputItem
*/
handleScanFile = (scanFileType, scanFileAddress) => {
this.setState({
showScanFile: true,
scanFileAddress,
scanFileType,
});
};
/**
* 添加选项
*
* @memberof QuestionInputItem
*/
handleAddOption = (content) => {
const { chooseOptions } = this.state;
if (chooseOptions.length >= 20) {
message.warning("最多添加20个选项");
} else {
chooseOptions.push(defineOptionInfo(content));
this._onSetState();
}
};
/**
* 初始化判断选项
*
* @memberof QuestionInputItem
*/
initJudgeOption = (content) => {
const { chooseOptions } = this.state;
chooseOptions.push(defineJudgeOptionInfo(content));
this._onSetState();
};
/**
* 删除选项
*
* @memberof QuestionInputItem
*/
handleDelOption = (optionIndex) => {
const { chooseOptions } = this.state;
this.handleLogger("delete_option", "删除选项");
if (chooseOptions.length < 3) {
message.warning("至少保留2个选项");
} else {
chooseOptions.splice(optionIndex, 1);
this._onSetState();
}
};
/**
* 移动选项
*
* @memberof QuestionInputItem
*/
handleMoveOption = (optionIndex, moveLength) => {
const { chooseOptions } = this.state;
const optionItem = chooseOptions.splice(optionIndex + moveLength, 1);
this.handleLogger("sort_option", "选项排序");
chooseOptions.splice(optionIndex, 0, optionItem[0]);
this._onSetState();
};
/**
* 选择上传文件类型
*
* @memberof QuestionInputItem
*/
handleChangeMedia = (key, uploadItemTarget, contentType) => {
const pictureMediaArr = _.filter(uploadItemTarget, (mediaItem) => {
return mediaItem.type === "PICTURE";
});
const voiceMediaArr = _.filter(uploadItemTarget, (mediaItem) => {
return mediaItem.type === "VOICE";
});
const audioMediaArr = _.filter(uploadItemTarget, (mediaItem) => {
return mediaItem.type === "AUDIO";
});
const videodMediaArr = _.filter(uploadItemTarget, (mediaItem) => {
return mediaItem.type === "VIDEO";
});
switch (contentType) {
case "QUESTION_STEM":
var existType = [];
if (pictureMediaArr.length > 0) {
existType.push("PICTURE");
}
if (voiceMediaArr.length > 0) {
existType.push("VOICE");
}
if (audioMediaArr.length > 0) {
existType.push("AUDIO");
}
if (videodMediaArr.length > 0) {
existType.push("VIDEO");
}
if (existType.length > 0 && !existType.includes(key)) {
return message.warning("只能添加1种类型的多媒体文件");
} else {
if (key === "PICTURE" && pictureMediaArr.length > 8) {
return message.warning("只能添加9张图片");
}
if (key === "VOICE" && voiceMediaArr.length > 2) {
return message.warning("只能添加3个音频");
}
if (key === "AUDIO" && audioMediaArr.length > 2) {
return message.warning("只能添加3个录音");
}
if (key === "VIDEO" && videodMediaArr.length > 2) {
return message.warning("只能添加3个视频");
}
}
break;
case "QUESTION_OPTION":
var existType = [];
if (pictureMediaArr.length > 0) {
existType.push("PICTURE");
}
if (voiceMediaArr.length > 0) {
existType.push("VOICE");
}
if (audioMediaArr.length > 0) {
existType.push("AUDIO");
}
if (videodMediaArr.length > 0) {
existType.push("VIDEO");
}
if (existType.length > 0) {
return message.warning("只能添加1个多媒体文件");
}
break;
case "QUESTION_ANSWER_DESC":
var existType = [];
if (pictureMediaArr.length > 0) {
existType.push("PICTURE");
}
if (voiceMediaArr.length > 0) {
existType.push("VOICE");
}
if (audioMediaArr.length > 0) {
existType.push("AUDIO");
}
if (videodMediaArr.length > 0) {
existType.push("VIDEO");
}
if (existType.length > 2 && !existType.includes(key)) {
return message.warning("只能添加3种类型的多媒体文件");
} else {
if (key === "PICTURE" && pictureMediaArr.length > 8) {
return message.warning("只能添加9张图片");
}
if (key === "VOICE" && voiceMediaArr.length > 2) {
return message.warning("只能添加3个音频");
}
if (key === "AUDIO" && audioMediaArr.length > 2) {
return message.warning("只能添加3个录音");
}
if (key === "VIDEO" && videodMediaArr.length > 2) {
return message.warning("只能添加3个视频");
}
}
break;
}
var that = this;
function change() {
that.setState({ contentType });
that.setState({ mediaType: key });
that.setState(
{
uploadItemTarget,
},
() => {
MEDIA_FILE_ACCEPT[key] &&
that.setState(
{
accept: MEDIA_FILE_ACCEPT[key],
fileType: key,
},
() => {
that.uploadInput.current.value = "";
that.setState({ showSelectFileModal: key !== "AUDIO" });
}
);
}
);
}
if (key == "AUDIO") {
// 录音
this.setState({
showRecord: true,
});
this.setState({
onAudioFinish: function () {
change();
},
});
} else {
change();
}
};
async uploadFile(mediaFile) {
const { fileType, uploadItemTarget, contentType } = this.state;
if (!mediaFile) return;
if (fileType === "VOICE") {
if (
!MEDIA_FILE_ACCEPT.VOICE.split(",").includes(mediaFile.folderFormat)
) {
message.warning("文件格式不正确");
return;
}
if (mediaFile.size > 20 * 1024 * 1024) {
Modal.warning({
title: "音频过大",
content: "音频大小超过20M,请压缩后上传",
});
return;
}
}
if (fileType === "PICTURE") {
if (
!MEDIA_FILE_ACCEPT.PICTURE.split(",").includes(mediaFile.folderFormat)
) {
message.warning("文件格式不正确");
return;
}
if (mediaFile.folderSize > 5 * 1024 * 1024) {
Modal.warning({
title: "图片过大",
content: "图片大小超过5M,请压缩后上传",
});
return;
}
}
if (fileType === "VIDEO") {
if (
!MEDIA_FILE_ACCEPT.VIDEO.split(",").includes(mediaFile.folderFormat)
) {
message.warning("文件格式不正确");
return;
}
if (mediaFile.folderSize > 500 * 1024 * 1024) {
Modal.warning({
title: "视频过大",
content: "视频大小超过500G,请压缩后上传",
});
return;
}
}
const originArr = mediaFile.folderName.split(".");
const originType = originArr[originArr.length - 1];
const uploadObj = {
contentType,
type: fileType,
contentName: `${window.random_string(16)}.${originType}`, // 文件名
fileType: mediaFile.folderFormat, // 文件后缀
content: mediaFile.ossUrl, // url
};
if (["VIDEO", "VOICE"].includes(fileType)) {
try {
await new Promise((resolve) => {
const fileurl = URL.createObjectURL(mediaFile);
const audioElement = new Audio(fileurl);
audioElement.addEventListener("loadedmetadata", (_event) => {
const duration = audioElement.duration;
uploadObj.size = Math.ceil(duration) * 1000;
resolve();
});
});
} catch (error) {
console.log(error);
}
} else if (fileType === "PICTURE") {
uploadObj.size = mediaFile.folderSize;
}
uploadItemTarget.push(uploadObj);
this.setState({}, () => {
this._onSetState();
this.uploadInput.current.value = "";
});
}
changeBlankCount = (data, idx) => {
let _gap = this.state.gapFillingAnswer;
if (data.length <= idx) {
_gap.splice(idx, 1);
} else {
data.forEach((item, index) => {
if (index === idx) {
if (_gap.length < data.length) {
_gap.splice(idx, 0, { correctAnswerList: [] });
} else if (_gap.length > data.length) {
_gap.splice(idx, 1);
} else {
_gap.splice(idx, 1, { correctAnswerList: [] });
}
}
if (!item.correctAnswerList) {
item.correctAnswerList = [];
}
item.inputVisible = false;
item.errorHold = false;
item.editInput = false;
return item;
});
}
this.setState(
{
blanksList: data,
gapFillingAnswer: _gap,
},
() => this._onSetState()
);
};
addAnswerTag = (optionItem) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
item.inputVisible = true;
}
});
this.setState({ blanksList: _blanksList });
};
// 填空选项
handleInputConfirm = (optionItem, val) => {
var tagContent = val.replace(/\&nbsp\;/gi, "");
tagContent = val.replace(/\s+/g, "");
const _blanksList = this.state.blanksList;
const { gapFillingAnswer } = this.state;
let _gapFillingAnswer = [...gapFillingAnswer];
_blanksList.forEach((item, index) => {
if (item.id === optionItem.id) {
if (val && tagContent.length > 0) {
_gapFillingAnswer[index].correctAnswerList.push(val);
optionItem.inputVisible = false;
} else {
optionItem.errorHold = true;
}
}
});
this.setState({ gapFillingAnswer: _gapFillingAnswer }, () =>
this._onSetState()
);
this.setState({ blanksList: _blanksList });
};
handleTagClose = (optionItem, removedTag, removedIndex) => {
const _blanksList = this.state.blanksList;
const { gapFillingAnswer } = this.state;
let _gapFillingAnswer = [...gapFillingAnswer];
_blanksList.forEach((item, index) => {
if (item.id === optionItem.id) {
_gapFillingAnswer[index].correctAnswerList = _gapFillingAnswer[
index
].correctAnswerList.filter(
(tag, index) => tag !== removedTag || index !== removedIndex
);
}
});
this.setState({ gapFillingAnswer: _gapFillingAnswer }, () =>
this._onSetState()
);
this.setState({ blanksList: _blanksList });
};
// 输入框关闭
handleInputClose = (optionItem) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
item.inputVisible = false;
optionItem.errorHold = false;
}
});
this.setState({ blanksList: _blanksList });
};
handleInputEdit = (optionItem, index) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
item.correctAnswerList.map();
}
});
this.setState({ blanksList: _blanksList });
};
renderGapFillingAnswer = (optionItem, optionIndex) => {
const { gapFillingAnswer } = this.state;
const list =
gapFillingAnswer[optionIndex] &&
gapFillingAnswer[optionIndex].correctAnswerList;
return (
<div className="gap-answer-box" key={optionIndex}>
<span className="gap-answer-label">
填空
{optionIndex + 1}.
</span>
<div className="gap-answer-content">
{list &&
list.map((tag, index) => {
return (
<Tag
key={index}
className="edit-tag"
visible
closable
onClose={() => this.handleTagClose(optionItem, tag, index)}
>
{tag}
</Tag>
);
})}
{optionItem.inputVisible && (
<Input
placeholder={optionItem.errorHold && "请输入"}
style={{
border: optionItem.errorHold && "1px solid #FF4F4F",
}}
maxLength={60}
size="small"
suffix={
<CloseOutlined
onClick={() => this.handleInputClose(optionItem)}
style={{ color: "#999999" }}
/>
}
onBlur={(e) =>
this.handleInputConfirm(optionItem, e.target.value, "save")
}
onPressEnter={(e) =>
this.handleInputConfirm(optionItem, e.target.value, "save")
}
/>
)}
<Tag
style={{ color: "#5289FA", fontSize: 14 }}
onClick={() => {
this.addAnswerTag(optionItem);
}}
>
<PlusOutlined /> 新增答案
</Tag>
</div>
</div>
);
};
renderJudgeOption = (judgeOptions) => {
return (
<div dangerouslySetInnerHTML={{ __html: judgeOptions[0].content }} />
);
};
/**
* 渲染输入内容
*
* @memberof QuestionInputItem
*/
renderContent = (
contentList,
placehold,
mediaBtn,
contentType,
validateStatus
) => {
const { blanksList } = this.state;
const isGapFilling = this.props.questionTypeKey === "GAP_FILLING";
if (contentList.length === 0) {
contentList.push({
content: "",
contentType: "QUESTION_ANSWER_DESC",
sort: 0,
textLength: 0,
type: "RICH_TEXT",
});
}
const editorContent = _.find(
contentList,
(contentItem) => contentItem.type === "RICH_TEXT"
);
const pictureMediaList = _.filter(contentList, (mediaItem) => {
return mediaItem.type === "PICTURE";
});
const voiceMediaList = _.filter(contentList, (mediaItem) => {
return mediaItem.type === "VOICE";
});
const audioMediaList = _.filter(contentList, (mediaItem) => {
return mediaItem.type === "AUDIO";
});
const videoMediaList = _.filter(contentList, (mediaItem) => {
return mediaItem.type === "VIDEO";
});
return (
<React.Fragment>
<div>
<QuestionEditor
markKey={this.markKey}
placehold={placehold}
validateStatus={validateStatus}
detailInfo={editorContent}
isGapFilling={isGapFilling}
contentType={contentType}
mediaBtn={mediaBtn}
blanksList={blanksList}
changeBlankCount={this.changeBlankCount.bind(this)}
bindChangeContent={(cb, textElemId) => {
this.setState({ textElemId });
editorContent.handleChangeContent = cb;
}}
onChange={(content, textLength) => {
editorContent.content = content;
editorContent.textLength = textLength;
this._onSetState();
}}
onUploadMedia={(key) => {
this.handleChangeMedia(key, contentList, contentType);
}}
/>
</div>
{contentType === "QUESTION_ANSWER_DESC" ? (
<div className="question-desc-box">
{pictureMediaList.length > 0 && (
<div className="desc-picture-box">
{_.map(pictureMediaList, (pictureItem, pictureIndex) => {
let { content } = pictureItem;
return (
<div className="picture-box" key={pictureIndex}>
<img
className="img-box"
src={content}
onClick={() => this.handleScanFile("JPG", content)}
/>
<span
className="icon_arrow iconfont"
onClick={() => {
contentList.map((item, index) => {
if (item.contentName === pictureItem.contentName) {
contentList.splice(index, 1);
return item;
}
});
this._onSetState();
}}
>
&#xe717;
</span>
</div>
);
})}
</div>
)}
{audioMediaList.length > 0 && (
<div className="desc-audio-box">
{_.map(audioMediaList, (audioItem, audioIndex) => {
let { content, size } = audioItem;
return (
<div className="audio-box" key={audioIndex}>
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={audioIndex}
size={size || 1000}
/>
<span
className="icon_sider iconfont"
onClick={() => {
contentList.map((item, index) => {
if (item.contentName === audioItem.contentName) {
contentList.splice(index, 1);
return item;
}
});
this._onSetState();
}}
>
&#xe717;
</span>
</div>
);
})}
</div>
)}
{voiceMediaList.length > 0 && (
<div className="desc-audio-box">
{_.map(voiceMediaList, (voiceItem, voiceIndex) => {
let { content, size } = voiceItem;
return (
<div className="audio-box" key={voiceIndex}>
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={voiceIndex}
size={size || 1000}
/>
<span
className="icon_sider iconfont"
onClick={() => {
contentList.map((item, index) => {
if (item.contentName === voiceItem.contentName) {
contentList.splice(index, 1);
return item;
}
});
this._onSetState();
}}
>
&#xe717;
</span>
</div>
);
})}
</div>
)}
{videoMediaList.length > 0 && (
<div className="desc-video-box">
{_.map(videoMediaList, (videoItem, videoIndex) => {
let { content } = videoItem;
return (
<div className="video-box" key={videoIndex}>
<img
className="video-box_content"
src={`${content}?x-oss-process=video/snapshot,t_0,m_fast`}
/>
<img
className="video-box_btn"
src="https://image.xiaomaiketang.com/xm/r5H8cYm4ch.png"
onClick={() => this.handleScanFile("MP4", content)}
/>
<span
className="icon_arrow iconfont"
onClick={() => {
contentList.map((item, index) => {
if (item.contentName === videoItem.contentName) {
contentList.splice(index, 1);
return item;
}
});
this._onSetState();
}}
>
&#xe717;
</span>
</div>
);
})}
</div>
)}
</div>
) : (
_.map(contentList, (contentItem, index) => {
const { type, content } = contentItem;
let dom = "";
switch (type) {
case "PICTURE":
dom = (
<div className="picture-box">
<img
className="img-box"
src={content}
onClick={() => this.handleScanFile("JPG", content)}
/>
</div>
);
break;
case "VOICE":
dom = (
<div className="audio-box">
<XMAudio
forbidParse
url={contentItem.content}
getDuration={(size) => {
contentItem.size = size;
this.setState({});
}}
index={index}
size={contentItem.size || 1000}
/>
</div>
);
break;
case "AUDIO":
dom = (
<div className="audio-box">
<XMAudio
forbidParse
url={contentItem.content}
getDuration={(size) => {
contentItem.size = size;
this.setState({});
}}
index={index}
size={contentItem.size || 1000}
/>
</div>
);
break;
case "VIDEO":
dom = (
<div
className="video-box"
onClick={() => this.handleScanFile("MP4", content)}
>
<img
className="video-box_content"
src={`${content}?x-oss-process=video/snapshot,t_0,m_fast`}
/>
<img
className="video-box_btn"
src="https://image.xiaomaiketang.com/xm/r5H8cYm4ch.png"
/>
</div>
);
break;
}
return dom ? (
<div
className="question-item_question-content"
style={{
display: ["PICTURE", "VIDEO"].includes(type)
? "inline-grid"
: "flex",
}}
key={index}
>
{dom}
<span
className={
["PICTURE", "VIDEO"].includes(type)
? "icon_arrow iconfont"
: "icon_sider iconfont"
}
onClick={() => {
contentList.splice(index, 1);
this._onSetState();
}}
>
&#xe717;
</span>
</div>
) : null;
})
)}
</React.Fragment>
);
};
/**
* 取消上传
*
* @memberof QuestionInputItem
*/
handleAbort = (uploadItem, index) => {
const { uploadItemTarget } = this.state;
const { xhr } = uploadItem;
xhr && xhr.abort && xhr.abort();
uploadItemTarget.splice(index, 1);
this._onSetState();
};
/**
* 完成语音录制
*
* @memberof QuestionInputItem
*/
handleFinishRecord = (mp3URL, duration) => {
const originArr = mp3URL.split(".");
const originType = originArr[originArr.length - 1];
this.state.onAudioFinish();
const { uploadItemTarget, contentType } = this.state;
uploadItemTarget.push({
contentType,
type: "AUDIO",
contentName: `${window.random_string(16)}.${originType}`, // 文件名
fileType: originType, // 文件后缀
content: mp3URL,
size: duration,
});
this._onSetState({ showRecord: false });
};
/**
* 取消录制
*
* @memberof QuestionInputItem
*/
handleCancelRecord = () => {
this.setState({ showRecord: false });
};
handleSelectMedia = (file) => {
this.uploadFile(file);
this.setState({
showSelectFileModal: false,
});
};
render() {
const {
stemContent,
chooseOptions,
questionAnswerDesc,
accept,
showRecord,
showScanFile,
scanFileType,
scanFileAddress,
blanksList,
showSelectFileModal,
mediaType,
diskList,
} = this.state;
const { stemValidate, stemText, radioValidate, radioText } = this.state;
const isJudge = this.props.questionTypeKey === "JUDGE";
const isGapFilling = this.props.questionTypeKey === "GAP_FILLING";
const placehold = isGapFilling ? (
<span>
示例:党章规定,凡事有
<span className="fill-line">&nbsp;&nbsp;填空1&nbsp;&nbsp;</span>
人以上的
<span className="fill-line">&nbsp;&nbsp;填空2&nbsp;&nbsp;</span>
,都应该成立党的基层组织
</span>
) : (
"必填(1000字以内)"
);
let acceptType = "";
let selectTypeList = [];
switch (mediaType) {
case "PICTURE":
acceptType = MEDIA_FILE_ACCEPT.PICTURE;
selectTypeList = ["jpg", "png", "jpeg"];
break;
case "VOICE":
acceptType = MEDIA_FILE_ACCEPT.VOICE;
selectTypeList = ["mp3", "mpeg"];
break;
case "VIDEO":
acceptType = MEDIA_FILE_ACCEPT.VIDEO;
selectTypeList = ["mp4"];
break;
}
return (
<div className="question-input-item_wrapper">
{/* 题干 */}
<Form>
<Form.Item
name="stemContent"
label="题干"
required
validateStatus={stemValidate}
help={stemText}
>
{this.renderContent(
stemContent,
placehold,
["VOICE", "AUDIO", "PICTURE"],
"QUESTION_STEM",
stemValidate
)}
</Form.Item>
{isGapFilling ? (
<Form.Item
name="answer"
label={
<span>
答案{" "}
<Popover
content={
<div>
<div style={{ marginBottom: "16px" }}>
一空可设置多个答案,答对任意一个即视为正确
</div>
<img
width="400px"
src="https://image.xiaomaiketang.com/xm/gaptip.png"
alt=""
/>
</div>
}
>
<span
className="icon iconfont"
style={{ color: "#BFBFBF", fontSize: 14 }}
>
&#xe7c4;
</span>
</Popover>
</span>
}
required
className="question-answer_control"
>
{blanksList.length === 0 ? (
<span className="answer-tip">请在题干中插入答案占位符</span>
) : (
_.map(blanksList, (item, index) => {
return (
<Form.Item
name="answer"
validateStatus={this.state[`optionsValidate_${index}`]}
help={this.state[`optionsText_${index}`]}
>
{this.renderGapFillingAnswer(item, index)}
</Form.Item>
);
})
)}
</Form.Item>
) : (
<Form.Item
name="options"
label="选项"
required
className="question-option_control"
>
<div
className="question-item_options__list"
data-label="正确答案"
>
{_.map(chooseOptions, (optionItem, optionIndex) => {
const {
questionOptionContentList,
isCorrectAnswer,
} = optionItem;
optionItem.optionSort = optionIndex;
const mediaBtn = ["VOICE", "AUDIO", "PICTURE"];
const placeHold =
"必填(1000字以内;可以不输入文字,只添加音频或图片)";
return (
<div
className="question-item_options__content"
key={optionIndex}
>
<div className="question-item_options__setting">
<Form.Item
validateStatus={radioValidate}
help={
optionIndex === chooseOptions.length - 1
? radioText
: ""
}
>
{/* 单选 or 判断*/}
{["SINGLE_CHOICE", "JUDGE"].includes(
this.props.questionTypeKey
) && (
<Radio
checked={isCorrectAnswer}
onClick={() => {
_.each(chooseOptions, (o) => {
o.isCorrectAnswer = 0;
});
optionItem.isCorrectAnswer = 1;
this._onSetState();
}}
/>
)}
{/* 多选 or 不定项 */}
{["INDEFINITE_CHOICE", "MULTI_CHOICE"].includes(
this.props.questionTypeKey
) && (
<Checkbox
checked={isCorrectAnswer === 1}
onChange={(e) => {
const checked = e.target.checked ? 1 : 0;
optionItem.isCorrectAnswer = checked;
this._onSetState();
}}
/>
)}
</Form.Item>
</div>
<div className="question-item_options__sort mr12">
{NUM_TO_WORD_MAP[optionIndex]}.
</div>
<div className="question-item_options__input">
{isJudge ? (
this.renderJudgeOption(questionOptionContentList)
) : (
<Form.Item
validateStatus={
this.state[`optionsValidate_${optionIndex}`]
}
help={this.state[`optionsText_${optionIndex}`]}
>
{this.renderContent(
questionOptionContentList,
placeHold,
mediaBtn,
"QUESTION_OPTION",
this.state[`optionsValidate_${optionIndex}`]
)}
</Form.Item>
)}
</div>
{[
"INDEFINITE_CHOICE",
"MULTI_CHOICE",
"SINGLE_CHOICE",
].includes(this.props.questionTypeKey) && (
<div className="question-item_options__extra">
<React.Fragment>
<span
className="option-operate_item__icon icon iconfont"
style={{ fontSize: "14px" }}
onClick={() => this.handleDelOption(optionIndex)}
>
&#xe81a;
</span>
{optionIndex > 0 && (
<span
className="option-operate_item__icon icon iconfont"
onClick={() =>
this.handleMoveOption(optionIndex, -1)
}
>
&#xe74a;
</span>
)}
{optionIndex < chooseOptions.length - 1 && (
<span
className="option-operate_item__icon icon iconfont"
style={{
transform: "rotate(180deg)",
display: "inline-block",
}}
onClick={() =>
this.handleMoveOption(optionIndex, 1)
}
>
&#xe74a;
</span>
)}
</React.Fragment>
</div>
)}
</div>
);
})}
{!isJudge && (
<div
className="question-item_options__add"
onClick={() => {
this.handleAddOption();
}}
>
+ 增加选项
</div>
)}
</div>
</Form.Item>
)}
<Form.Item name="analysis" label="答案解析">
<div className="question-item_analysis__content">
{this.renderContent(
questionAnswerDesc,
"1000字以内",
["VOICE", "AUDIO", "PICTURE", "VIDEO"],
"QUESTION_ANSWER_DESC"
)}
</div>
</Form.Item>
</Form>
<input
type="file"
accept={accept}
ref={this.uploadInput}
style={{ display: "none" }}
onChange={(event) => {
this.uploadFile(event);
}}
/>
<div style={{ zIndex: 9999, position: "relative" }}>
<XMRecord
maxTime={600}
visible={showRecord}
onFinish={this.handleFinishRecord}
onCancel={this.handleCancelRecord}
/>
</div>
{showScanFile && (
<ScanFileModal
modalTitle={scanFileType === "MP4" ? "视频播放" : "查看大图"}
fileType={scanFileType}
item={{
ossAddress: scanFileAddress,
}}
close={() => {
this.setState({ showScanFile: false });
}}
/>
)}
<SelectPrepareFileModal
operateType="select"
accept={acceptType}
selectTypeList={selectTypeList}
isOpen={showSelectFileModal}
diskList={diskList}
onClose={() => {
this.setState({ showSelectFileModal: false });
}}
onSelect={this.handleSelectMedia}
/>
</div>
);
}
}
export default NewQuestionTab;
.question-input-item_wrapper {
border-radius: 2px;
padding: 16px;
position: relative;
margin-bottom: 35px;
.editor-fill-box_single {
border-radius: 4px;
padding: 4px 10px;
border: 1px solid #e8e8e8;
.fill-line {
padding: 0 10px;
border-bottom: 1px solid;
}
}
.editor-fill-box_single:focus {
border: 1px solid #e8e8e8;
outline: none;
}
.fill-info {
height: 20px;
font-size: 14px;
line-height: 20px;
color: #999999;
margin-top: 8px;
.fill-info_icon {
color: #5289fa;
font-size: 14px;
padding-left: 9px;
cursor: pointer;
}
}
&__active {
border-color: #ff8534;
}
.ant-form-item-explain,
.ant-form-item-extra {
font-size: 12px;
width: 78px;
min-height: 0;
}
.question-option_control {
margin: 40px 0 !important;
}
.question-option_control > .ant-form-item-control {
margin-top: 27px;
}
.question-answer_control {
margin: 40px 0 24px !important;
.answer-tip {
font-size: 14px;
color: #cccccc;
}
}
.question-item_question-content {
position: relative;
margin: 12px 12px 12px 0;
.picture-box {
width: 88px;
height: 88px;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e8e8e8;
}
.audio-box {
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
}
.img-box {
max-width: 88px;
max-height: 88px;
border-radius: 4px;
}
.video-box {
width: 200px;
height: calc(200px * 9 / 16);
position: relative;
border-radius: 4px;
overflow: hidden;
background-color: #000;
&_content {
max-width: 100%;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&_btn {
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -16px;
margin-left: -16px;
}
}
.icon_arrow {
position: absolute;
top: -12px;
right: -7px;
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
}
.icon_sider {
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
padding: 12px;
}
}
.question-item_control {
position: absolute;
top: 8px;
right: 0;
.icon {
font-size: 18px;
color: rgba(0, 0, 0, 0.4);
cursor: pointer;
&:hover {
color: #ff8534;
}
}
}
.question-item_label {
font-size: 14px;
color: #333333;
line-height: 33px;
flex-shrink: 0;
align-self: stretch;
}
.question-item_number {
display: inline-block;
background: rgba(216, 216, 216, 0.3);
border-radius: 2px;
font-size: 14px;
color: #333333;
line-height: 20px;
padding: 4px 8px;
}
.question-item_score {
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 20px;
}
.question-item_main {
display: flex;
align-items: center;
}
.question-item_main__content {
flex: 1;
margin-right: 187px;
}
// .question-item_options {
// display: flex;
// align-items: center;
// padding-bottom: 15px;
// }
.question-item_options__list {
flex: 1;
position: relative;
&::before {
content: attr(data-label);
position: absolute;
left: 15px;
transform: translateY(-100%);
font-size: 12px;
color: #666666;
line-height: 22px;
}
.ant-radio-wrapper {
margin-right: 0;
}
}
.question-item_true-false {
&::before {
transform: translateY(0);
top: 10px;
}
}
.question-item_options__content {
display: flex;
align-items: center;
}
.question-item_options__trur-false {
display: flex;
align-items: center;
height: 33px;
}
.question-item_options__sort {
flex-shrink: 0;
align-self: stretch;
line-height: 32px;
}
.question-item_options__input {
flex: 1;
}
.question-item_options__extra {
flex-shrink: 0;
padding: 0 20px 0 3px;
width: 108px;
line-height: 33px;
align-self: stretch;
.option-operate_item__icon:hover {
color: #ffb714;
}
.icon {
color: #bfbfbf;
margin-left: 10px;
cursor: pointer;
font-size: 16px;
}
}
.question-item_options__setting {
flex-shrink: 0;
width: 80px;
height: 32px;
text-align: center;
align-self: stretch;
}
.question-item_options__add {
height: 44px;
border-radius: 4px;
border: 1px dashed #e8e8e8;
font-size: 14px;
color: #5289fa;
line-height: 44px;
text-align: center;
cursor: pointer;
margin-right: 109px;
margin-left: 106px;
}
.question-item_other {
border-top: 1px solid #eeeeee;
padding-top: 15px;
}
.question-item_setting-score {
display: flex;
align-items: center;
}
.question-item_analysis {
display: flex;
align-items: center;
}
.question-item_analysis__content {
flex: 1;
.question-desc-box {
margin-top: 24px;
.desc-picture-box {
margin-bottom: 16px;
.picture-box {
position: relative;
display: inline-flex;
width: 88px;
height: 88px;
border: 1px solid #e8e8e8;
margin-right: 12px;
img {
max-width: 100%;
max-height: 100%;
border-radius: 4px;
vertical-align: middle;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.icon_arrow {
position: absolute;
top: -12px;
right: -8px;
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
}
}
}
.desc-audio-box {
margin-bottom: 28px;
.audio-box {
position: relative;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
width: 320px;
margin-bottom: 12px;
.icon_sider {
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
padding: 12px;
position: absolute;
top: 0px;
left: 320px;
}
}
}
.desc-video-box {
.video-box {
position: relative;
display: inline-block;
width: 208px;
height: calc(208px * 9 / 16);
position: relative;
background-color: #000;
margin: 0px 12px 12px 0;
&_content {
max-width: 100%;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&_btn {
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -16px;
margin-left: -16px;
}
.icon_arrow {
position: absolute;
top: -12px;
right: -8px;
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
}
}
}
}
}
.gap-answer-box {
display: inline-flex;
width: 100%;
padding: 6px 0;
.gap-answer-label {
margin-right: 12px;
white-space: nowrap;
}
.gap-answer-content {
background: #ffffff;
border-radius: 4px;
border: 1px solid #e8e8e8;
padding: 3px 12px;
width: calc(100% - 50px);
word-wrap: break-word;
word-break: break-all;
overflow: hidden;
margin-top: -5px;
.ant-tag {
border: none;
font-size: 14px;
line-height: 24px;
vertical-align: middle;
}
.gap-tag-input {
margin-right: 5px;
border: 1px solid rgb(165, 165, 165);
}
.ant-tag-close-icon {
font-size: 14px;
}
::-webkit-input-placeholder {
/* WebKit browsers */
color: #ff4f4f;
}
::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: #ff4f4f;
}
:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: #ff4f4f;
}
.ant-input-affix-wrapper {
border: none;
background: #f7f8f9;
:hover {
border-color: none;
}
width: 78px;
margin-right: 14px;
.ant-input {
background: #f7f8f9;
}
.ant-input:not(:last-child) {
padding-right: 0px !important;
}
.ant-input:focus {
border: none !important;
}
}
}
}
}
.question_skeleton {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%);
background-size: 400% 100%;
animation: question-editor_skeleton__loading 1.4s ease infinite;
}
.question_skeleton__editor {
min-height: 33px;
max-height: 140px;
overflow: hidden;
}
.question_skeleton__img {
width: 88px;
height: 88px;
}
.question_skeleton__voice {
height: 48px;
width: 280px;
}
.question_skeleton__video {
width: 100%;
height: 100%;
}
@keyframes question-editor_skeleton__loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}
/*
* @Author: yuananting
* @Date: 2021-02-22 10:59:43
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 14:50:08
* @Description: 助学工具-题库-题库主页面侧边栏
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Input, Button, Tree } from "antd";
import "./QuestionBankSider.less";
import User from "@/common/js/user";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
const { Search } = Input;
const { DirectoryTree } = Tree;
class QuestionBankSider extends Component {
constructor(props) {
super(props);
const categoryId = getParameterByName("categoryId");
this.state = {
selectedKeys: categoryId
? categoryId === "null"
? ["0"]
: [categoryId]
: ["0"],
searchValue: null,
NewEditQuestionBankCategory: null, //新增或编辑分类模态框
ImportCourseCategory: null, // 引用课程分类模态框
treeData: this.props.treeData || [],
autoExpandParent: false,
};
}
componentDidMount() {
this.queryCategoryTree("change");
this.props.getSelectedCategoryId(
getParameterByName("categoryId")
? [getParameterByName("categoryId")]
: ["0"]
);
}
shouldComponentUpdate(nextProps, nextState) {
const { currentTotal, updatedCategoryId } = nextProps;
if (
this.props.currentTotal !== currentTotal &&
this.props.updatedCategoryId === updatedCategoryId
) {
this.queryCategoryTree("remain");
}
return true;
}
/** 获取树状第一级key 设置默认展开第一项 */
getFirstLevelKeys = (data) => {
let firstLevelKeys = [];
data.forEach((item) => {
if (item.categoryLevel === 0) {
firstLevelKeys.push(item.key);
}
});
return firstLevelKeys;
};
/** 树状展开事件 */
onExpand = (expandedKeys) => {
this.setState({ expandedKeys });
};
/** 树状选中事件 */
onSelect = (selectedKeys) => {
this.setState({ selectedKeys });
this.props.getSelectedCategoryId(selectedKeys);
};
// 查询分类树
queryCategoryTree = (type, categoryName) => {
let query = {
source: 0,
categoryName,
userId: User.getStoreUserId(),
tenantId: User.getStoreId(),
};
QuestionBankService.queryQuestionCategoryTree(query).then((res) => {
const { categoryList = [], noCategoryCnt = 0 } = res.result;
let str = "未分类";
if (categoryName) {
this.setState({ autoExpandParent: true });
if (str.indexOf(categoryName) < 0) {
this.setState({
treeData: this.renderTreeNodes(categoryList, categoryName),
});
let nodeId = [];
Object.keys(this.state.treeMap).forEach((item) => {
nodeId.push(item);
});
if (type === "change") {
this.setState({ expandedKeys: nodeId });
}
} else {
const defaultNode = {
id: "0",
categoryName: "未分类",
categoryCount: noCategoryCnt,
parentId: "0",
categoryLevel: 0,
};
categoryList.unshift(defaultNode);
this.setState({
treeData: this.renderTreeNodes(categoryList, categoryName),
});
let nodeId = [];
Object.keys(this.state.treeMap).forEach((item) => {
nodeId.push(item);
});
if (type === "change") {
this.setState({ expandedKeys: nodeId });
}
}
} else {
this.setState({ autoExpandParent: false });
const defaultNode = {
id: "0",
categoryName: "未分类",
categoryCount: noCategoryCnt,
parentId: "0",
categoryLevel: 0,
};
categoryList.unshift(defaultNode);
this.setState({
treeData: this.renderTreeNodes(categoryList, categoryName),
});
if (type === "change") {
this.setState({ expandedKeys: [] });
}
}
});
};
getTreeMap = (data, map) => {
data.forEach((item) => {
map[item.id] = item;
if (item.sonCategoryList && item.sonCategoryList.length > 0) {
this.getTreeMap(item.sonCategoryList, map);
}
});
return map;
};
renderTreeNodes = (data, value) => {
let newTreeData = data.map((item) => {
item.title = item.categoryName;
item.key = item.id;
item.title =
!value || (value && item.categoryName.indexOf(value) > -1) ? (
<span>
{item.categoryName}{item.categoryCount}
</span>
) : (
<span style={{ opacity: 0.5 }}>
{item.categoryName}{item.categoryCount}
</span>
);
item.icon =
item.categoryName === "未分类" ? (
<img
style={{ width: "24px", height: "24px", opacity: !value || (value && item.categoryName.indexOf(value) > -1) ? 1 : 0.5 }}
src="https://image.xiaomaiketang.com/xm/defaultCategory.png"
alt=""
/>
) : (
<img
style={{ width: "24px", height: "24px", opacity: !value || (value && item.categoryName.indexOf(value) > -1) ? 1 : 0.5 }}
src="https://image.xiaomaiketang.com/xm/hasCategory.png"
alt=""
/>
);
if (item.sonCategoryList) {
item.children = this.renderTreeNodes(item.sonCategoryList, value);
}
return item;
});
let map = {};
this.setState({ treeMap: this.getTreeMap(data, map) });
return newTreeData;
};
render() {
const {
treeData,
expandedKeys,
selectedKeys,
autoExpandParent,
} = this.state;
return (
<div className="question-bank-sider">
<div className="sider-title">题目分类</div>
<Search
className="sider-search"
placeholder="搜索名称分类"
onSearch={(value) => {
this.queryCategoryTree("change", value);
}}
enterButton={<span className="icon iconfont">&#xe832;</span>}
/>
{User.getUserRole() !== "CloudLecturer" && (
<div className="sider-btn">
<Button
onClick={() => {
window.RCHistory.push({
pathname: "/question-category-manage?from=aid",
});
}}
>
分类管理
</Button>
</div>
)}
<div className="sider-tree">
<DirectoryTree
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={this.onExpand}
selectedKeys={selectedKeys}
onSelect={this.onSelect}
showIcon
treeData={treeData}
/>
</div>
{this.state.NewEditQuestionBankCategory}
{this.state.ImportCourseCategory}
</div>
);
}
}
export default QuestionBankSider;
/*
* @Author: yuananting
* @Date: 2021-02-22 12:02:34
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 15:57:12
* @Description: 助学工具-题库-题库主页面侧边栏样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-bank-sider {
position: relative;
.sider-title {
height: 22px;
font-size: 16px;
font-weight: 500;
color: #000000;
line-height: 22px;
margin-bottom: 16px;
}
.sider-search {
margin-bottom: 16px;
width: 230px;
}
.sider-btn {
margin-bottom: 16px;
}
.sider-tree {
width: 240px;
overflow: scroll;
height: calc(100vh - 300px);
.empty-tree-tip {
text-align: center;
margin-top: 100%;
.empty-tree-btn {
color: #ffb714;
cursor: pointer;
}
}
.ant-tree.ant-tree-directory {
font-size: 14px;
font-weight: 400;
color: #666666;
width: 234px;
.anticon {
color: #999999;
}
.ant-tree-treenode {
height: 44px;
padding: 0;
span {
line-height: 44px;
white-space: nowrap;
}
.ant-tree-node-content-wrapper.ant-tree-node-selected {
color: #666666;
}
}
.ant-tree-treenode-selected:hover::before,
.ant-tree-treenode-selected::before {
background: #fffbf1;
}
}
}
.ant-tree .ant-tree-node-content-wrapper .ant-tree-iconEle {
line-height: 37px !important;
margin-right: 8px;
}
.ant-tree.ant-tree-directory .ant-tree-treenode:hover::before {
background-color: #F3F6FA;
}
}
import React, { Component } from "react";
import E from "wangeditor";
import { message, Button } from "antd";
import UploadOss from "@/core/upload";
import "./QuestionEditor.less";
const MEDIA_MAP = [
{
title: "音频",
icon: <React.Fragment>&#xe756;</React.Fragment>,
key: "VOICE",
},
{
title: "录音",
icon: <React.Fragment>&#xe7bb;</React.Fragment>,
key: "AUDIO",
},
{
title: "图片",
icon: <React.Fragment>&#xe758;</React.Fragment>,
key: "PICTURE",
},
{
title: "视频",
icon: <React.Fragment>&#xe755;</React.Fragment>,
key: "VIDEO",
},
];
class QuestionEditor extends Component {
constructor(props) {
super(props);
this.state = {
editorId: window.random_string(16),
visiblePlacehold: true,
visibleMediaBox: false,
zIndex: 9,
focusFlag: false,
isShowSingleInput: true,
contentLength: 0,
isGapFilling: props.isGapFilling,
contentType: props.contentType,
detailInfo: props.detailInfo || {},
blanksList: props.blanksList || [],
};
}
componentDidMount() {
this.renderEditor();
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
detailInfo: nextProps.detailInfo,
};
}
shouldComponentUpdate(nextProps, nextState) {
const { detailInfo, blanksList } = nextProps;
if (this.state.detailInfo !== detailInfo) {
this.setState({ detailInfo: nextProps.detailInfo }, () => {
this.renderEditor();
});
}
if (blanksList !== this.state.blanksList) {
this.setState({
blanksList,
});
}
return true;
}
handleUploadMedia = (key) => {
this.props.onUploadMedia && this.props.onUploadMedia(key);
};
getBoxDom = () => {
const { markKey } = this.props;
const domList = document.getElementsByClassName(
`question-edtior_box__${markKey}`
);
let zIndex = 99;
_.find(domList, (domItem, domIndex) => {
if (domItem.getAttribute("editorid") === this.state.editorId) {
zIndex = zIndex + domList.length - domIndex;
return domItem;
}
});
return zIndex;
};
handleChangeContent = (content) => {
content &&
this.editorRoot.txt.html(
/^\<p/.test(content) ? content : `<p>${content}</p>`
);
};
renderEditor() {
const { editorId, detailInfo } = this.state;
const { onChange, bindChangeContent } = this.props;
const editorRoot = new E(
`#editor${editorId}_tabbar`,
`#editor${editorId}_content`
);
editorRoot.config.menus = [];
editorRoot.config.uploadImgMaxSize = 1 * 1024 * 1024;
editorRoot.config.customAlert = function (info) {
message.warning(/1M/.test(info) ? "图片大于1M,请使用图片上传" : info);
};
editorRoot.config.customUploadImg = function (files, insert) {
// files 是 input 中选中的文件列表
// insert 是获取图片 url 后,插入到编辑器的方法
UploadOss.uploadBlobToOSS(files[0], window.random_string(16)).then(
(urlStr) => {
insert(urlStr);
}
);
};
editorRoot.config.zIndex = 999;
editorRoot.config.placeholder = "";
editorRoot.config.pasteFilterStyle = false;
editorRoot.config.pasteIgnoreImg = true;
editorRoot.config.focus = false;
// 自定义处理粘贴的文本内容
editorRoot.config.pasteTextHandle = function (content) {
if (content == "" && !content) return "";
var str = content;
str = str.replace(/<xml>[\s\S]*?<\/xml>/gi, "");
str = str.replace(/<style>[\s\S]*?<\/style>/gi, "");
str = str.replace(/<\/?[^>]*>/g, "");
str = str.replace(/[ | ]*\n/g, "\n");
str = str.replace(/\&nbsp\;/gi, " ");
str = str.replace(/[\r\n]/g, "");
if (str.length > 1000) {
str = str.substring(0, 1000);
message.error("内容过长,不能超过1000字");
}
return str;
};
let prevList = [];
let counter = 0;
const isEdit = getParameterByName("id");
if (isEdit) {
const stemDom = document.getElementsByClassName("add-fill-line");
prevList = [...stemDom].map((item) => item.id);
localStorage.setItem("gap_ques_prevList", JSON.stringify(prevList));
setTimeout(
function () {
const divHeight = document.getElementById(`editor${editorId}_content`)
.firstChild.offsetHeight;
if (divHeight > 30) {
this.setState({ isShowSingleInput: false });
} else {
this.setState({ isShowSingleInput: true });
}
}.bind(this)
);
}
editorRoot.config.onchange = (html) => {
const conLen = html.replace(/<(?!img|input).*?>/g, "").length;
counter++;
const { focusFlag } = this.state;
const textLength = editorRoot.txt.text().replace(/\&nbsp\;/gi, " ")
.length;
const imgLength = html.match(/<img/g)
? html.match(/<img/g).length * 2
: 0;
const contentLength = imgLength + textLength;
const divHeight = document.getElementById(`editor${editorId}_content`)
.firstChild.offsetHeight;
if (divHeight > 30 || imgLength > 0) {
this.setState({ isShowSingleInput: false });
} else {
this.setState({ isShowSingleInput: true });
}
if (
this.state.isGapFilling &&
this.state.contentType === "QUESTION_STEM"
) {
const stemHtml = this.transferStemDocument(html);
var _blanksList = stemHtml.getElementsByClassName("add-fill-line");
const ids = [..._blanksList].map((item) => item.id);
const isEdit = getParameterByName("id");
if (isEdit && counter === 1) {
const prev = localStorage.getItem("gap_ques_prevList");
prevList = prev && JSON.parse(prev);
}
let idx = 0;
if (prevList && ids) {
idx = this.getNewArr(prevList, ids);
const oldLen = prevList.length;
idx = idx >= oldLen ? idx - oldLen : idx;
}
prevList = [...ids];
this.setState({ blanksList: _blanksList }, () =>
this.props.changeBlankCount(_blanksList, ids.length > 0 ? idx : -1)
);
}
this.setState(
{
contentLength,
visiblePlacehold:
(conLen === 0 || (conLen === 1 && html === " ")) && !focusFlag,
},
() => {
onChange && onChange(html, this.state.contentLength);
}
);
};
editorRoot.config.onblur = () => {
this.setState({
focusFlag: false,
visibleMediaBox: false,
zIndex: 9,
});
};
editorRoot.create();
this.editorRoot = editorRoot;
const contentHtml = /^\<p/.test(detailInfo.content)
? detailInfo.content
: `<p>${detailInfo.content}</p>`;
editorRoot.txt.html(detailInfo.content);
const textLength = editorRoot.txt.text().replace(/\&nbsp\;/gi, " ").length;
const imgLength = contentHtml.match(/<img/g)
? contentHtml.match(/<img/g).length * 2
: 0;
const contentLength = imgLength + textLength;
this.setState(
{
contentLength,
visiblePlacehold: contentLength === 0 && !this.state.focusFlag,
},
() => {
onChange && onChange(contentHtml, this.state.contentLength);
}
);
bindChangeContent && bindChangeContent(this.handleChangeContent);
}
getNewArr(a, b) {
const arr = [...a, ...b];
const idx = arr.findIndex((item) => {
return !(a.includes(item) && b.includes(item));
});
return idx;
}
transferStemDocument = (txt) => {
const template = `<div class='option-content'>${txt}</div>`;
let doc = new DOMParser().parseFromString(template, "text/html");
let div = doc.querySelector(".option-content");
return div;
};
insertBlank = (blanks) => {
var blanks = `<input class="add-fill-line" disabled correctAnswerList="[]" id=${window.random_string(
16
)} value="填空"/>`;
this.editorRoot.cmd.do("insertHTML", blanks);
document.getSelection().collapseToEnd();
var _blanksList = [];
_blanksList = document.getElementsByClassName("add-fill-line");
this.setState({ blanksList: _blanksList });
this.props.changeBlankCount(_blanksList);
};
render() {
const {
editorId,
visiblePlacehold,
visibleMediaBox,
zIndex,
focusFlag,
contentLength,
isShowSingleInput,
isGapFilling,
contentType,
} = this.state;
const {
placehold,
mediaBtn = ["VOICE", "AUDIO", "PICTURE", "VIDEO"],
limitLength = 1000,
markKey,
} = this.props;
return (
<div
className={`question-edtior_box question-edtior_box__${markKey}`}
editorid={editorId}
style={{ zIndex }}
onMouseEnter={() => {
if (visibleMediaBox || focusFlag) return;
const setZIndex = this.getBoxDom();
this.setState({
visibleMediaBox: true,
zIndex: setZIndex,
});
}}
onMouseLeave={() => {
if (!visibleMediaBox || focusFlag) return;
this.setState({
visibleMediaBox: false,
zIndex: 9,
});
}}
>
<div
className="editor-box"
id={`editor${editorId}_tabbar`}
style={{ display: "none" }}
></div>
<div
className={
isShowSingleInput ? "editor-box-single " : "editor-box-multiple"
}
style={{
border:
this.props.validateStatus === "error" ||
contentLength > limitLength
? "1px solid red"
: "",
}}
>
<div
className="editor-box editor-box_content"
id={`editor${editorId}_content`}
></div>
<div className="editor-limit">
<span style={{ color: contentLength > limitLength ? "red" : "" }}>
{contentLength}
</span>
/{limitLength}
</div>
</div>
{isGapFilling && contentType === "QUESTION_STEM" && (
<div className="editor-fill-info">
在需要填写答案的地方
<span
className="editor-fill-info_icon icon iconfont"
onClick={this.insertBlank}
>
&#xe83e; 插入占位符
</span>
</div>
)}
<div
className={`editor-limit-tip${
contentLength > limitLength ? " mt6" : ""
}`}
style={{ height: contentLength > limitLength ? 20 : 0 }}
>
最多只能输入1000字
</div>
{visiblePlacehold && (
<div className="editor-placehold">{placehold}</div>
)}
{visibleMediaBox && !_.isEmpty(mediaBtn) && (
<div
className="edtior-media_box"
style={{
top:
isGapFilling && contentType === "QUESTION_STEM"
? "53%"
: "100%",
}}
>
<div className="edtior-media_list">
{_.map(mediaBtn, (mediaItem) => {
const mediaBtnMap = _.find(
MEDIA_MAP,
(mapItem) => mapItem.key === mediaItem
);
return (
mediaBtnMap && (
<div
className="edtior-media_item"
key={mediaItem}
onClick={() => this.handleUploadMedia(mediaItem)}
>
<div className="edtior-media_item__icon icon iconfont">
{mediaBtnMap.icon}
</div>
<div className="edtior-media_item__name">
{mediaBtnMap.title}
</div>
</div>
)
);
})}
</div>
</div>
)}
</div>
);
}
}
export default QuestionEditor;
.question-edtior_box {
position: relative;
z-index: 9;
background-color: #ffffff;
.add-fill-line {
padding: 0 10px;
border-bottom: 1px solid !important;
margin: 0 4px;
text-align: center;
border: none;
width: 54px;
background-color: transparent;
}
.editor-fill-info {
height: 20px;
font-size: 14px;
line-height: 20px;
color: #999999;
margin-top: 8px;
.ant-btn {
border: none;
.ant-input {
border-color: transparent;
}
}
.editor-fill-info_icon {
color: #5289fa !important;
font-size: 14px;
padding-left: 9px !important;
cursor: pointer;
}
}
.editor-box-single {
border-radius: 4px;
padding: 4px 0;
border: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
.editor-box_content {
width: calc(100% - 80px);
p {
display: inline-block;
}
}
.editor-limit {
padding-right: 12px;
line-height: 22px;
font-size: 12px;
color: #cccccc;
}
}
.editor-box-multiple {
border-radius: 4px;
padding: 4px 0;
border: 1px solid #e8e8e8;
.editor-box_content {
max-height: 110px;
overflow: auto;
p {
display: inline-block;
overflow-y: scroll;
}
}
.editor-limit {
text-align: right;
padding-right: 12px;
line-height: 22px;
font-size: 12px;
color: #cccccc;
padding-top: 3px;
}
}
.editor-limit-tip {
text-align: right;
color: #ec4b35;
transition: height 0.5s ease-in-out;
overflow: hidden;
}
.editor-box_content {
@media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) {
img {
zoom: calc(100% / 3);
}
}
@media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
img {
zoom: calc(100% / 2);
}
}
}
.w-e-text p {
margin: 0;
line-height: inherit;
}
.editor-placehold {
position: absolute;
top: 0px;
left: 0;
right: 0;
font-size: 14px;
color: #cccccc;
line-height: 22px;
margin: 6px 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
z-index: 999;
pointer-events: none;
}
.edtior-media_box {
position: absolute;
left: 0;
padding-top: 8px;
&::before {
content: "";
border: 6px solid transparent;
border-bottom-color: #ffffff;
position: absolute;
top: -3px;
left: 50%;
margin-left: -3px;
filter: drop-shadow(-2px -2px 4px rgba(0, 0, 0, 0.06));
}
.edtior-media_list {
background: #ffffff;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.06);
border-radius: 4px;
display: flex;
align-items: center;
padding: 12px 24px;
}
.edtior-media_item {
margin-right: 24px;
cursor: pointer;
&:hover {
.edtior-media_item__icon {
color: #ffb714;
}
}
&:last-child {
margin-right: 0;
}
.edtior-media_item__icon {
color: #999999;
text-align: center;
font-size: 20px;
}
.edtior-media_item__name {
text-align: center;
font-size: 12px;
color: #999999;
line-height: 17px;
}
}
}
}
/*
* @Author: yuananting
* @Date: 2021-02-25 11:23:47
* @LastEditors: yuananting
* @LastEditTime: 2021-03-25 14:32:22
* @Description: 助学工具-题库-题目管理主页面列表数据
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import {
Table,
Switch,
ConfigProvider,
Empty,
Row,
Input,
Select,
Tooltip,
Space,
Button,
Modal,
message,
} from "antd";
import { PageControl } from "@/components";
import "./QuestionManageContent.less";
import User from "@/common/js/user";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
import _ from "underscore";
import QuestionPreviewModal from "../modal/QuestionPreviewModal";
import BatchImportQuestionModal from "../modal/BatchImportQuestionModal";
const { Search } = Input;
const questionTypeEnum = {
SINGLE_CHOICE: "单选题",
MULTI_CHOICE: "多选题",
JUDGE: "判断题",
GAP_FILLING: "填空题",
INDEFINITE_CHOICE: "不定项选择题",
};
const questionTypeList = [
{
label: "单选题",
value: "SINGLE_CHOICE",
},
{
label: "多选题",
value: "MULTI_CHOICE",
},
{
label: "判断题",
value: "JUDGE",
},
{
label: "填空题",
value: "GAP_FILLING",
},
{
label: "不定项选择题",
value: "INDEFINITE_CHOICE",
},
];
class QuestionManageContent extends Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
order: "UPDATED_DESC", // 排序规则[ ACCURACY_DESC, ACCURACY_ASC, CREATED_DESC, CREATED_ASC, UPDATED_DESC, UPDATED_ASC ]
categoryId: null, // 当前题库分类Id
questionName: null, // 题目名称
questionType: null, // 题目类型
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
},
questionTypeList: [], // 题型列表
dataSource: [],
totalCount: 0,
QuestionPreviewModal: null, // 题目预览模态框
};
}
componentDidMount() {}
shouldComponentUpdate(nextProps, nextState) {
let { selectedCategoryId } = nextProps;
const _query = this.state.query;
if (this.props.selectedCategoryId !== selectedCategoryId) {
if (selectedCategoryId === "null") {
selectedCategoryId = null;
}
_query.categoryId = selectedCategoryId;
_query.questionName = null;
_query.questionType = null;
_query.current = 1;
this.setState({ query: _query }, () => this.queryQuestionPageList());
}
return true;
}
queryQuestionPageList = (remain) => {
const _query = this.state.query;
if (_query.categoryId === "0") _query.categoryId = null;
QuestionBankService.queryQuestionPageList(_query).then((res) => {
const { records = [], total = 0 } = res.result;
this.setState({ dataSource: records });
this.setState({ total }, () =>
this.props.updatedSiderTree(total, this.props.selectedCategoryId)
);
});
};
handleCreateQuestionBank = () => {
window.RCHistory.push({
pathname: `/create-new-question?categoryId=${this.state.query.categoryId}`,
});
};
delCategoryConfirm(record) {
return Modal.confirm({
title: "提示",
content: "确定要删除此题目吗?",
icon: (
<span className="icon iconfont default-confirm-icon">&#xe839; </span>
),
okText: "删除",
cancelText: "取消",
onOk: () => {
this.deleteQuestion(record);
},
});
}
deleteQuestion = (record) => {
let params = {
id: record.id,
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
QuestionBankService.deleteQuestion(params).then((res) => {
if (res.success) {
message.success("删除成功");
const { query, total } = this.state;
const { size, current } = query;
const _query = query;
if (total / size < current) {
if (total % size === 1) {
_query.current = 1;
}
}
this.setState({ query: _query }, () => this.queryQuestionPageList());
}
});
};
// 排序
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
let sort = null;
if (columnKey === "accuracy" && order === "ascend") {
sort = "ACCURACY_ASC";
}
if (columnKey === "accuracy" && order === "descend") {
sort = "ACCURACY_DESC";
}
if (columnKey === "updateTime" && order === "ascend") {
sort = "UPDATED_ASC";
}
if (columnKey === "updateTime" && order === "descend") {
sort = "UPDATED_DESC";
}
const _query = this.state.query;
_query.order = sort || "UPDATED_DESC";
_query.current = 1;
this.setState({ query: _query }, () => this.queryQuestionPageList());
};
// 清空搜索条件
handleReset = () => {
const _query = {
...this.state.query,
current: 1,
order: "ACCURACY_DESC", // 排序规则
questionName: null, // 题目名称
questionType: null, // 题目类型
};
this.setState({ query: _query }, () => {
this.queryQuestionPageList();
});
};
previewQuestion = (id) => {
const m = (
<QuestionPreviewModal
id={id}
close={() => {
this.setState({
QuestionPreviewModal: null,
});
}}
/>
);
this.setState({ QuestionPreviewModal: m });
};
toEditQuetion = (id, type) => {
const { categoryId } = this.state.query;
if (categoryId) {
window.RCHistory.push({
pathname: `/create-new-question?id=${id}&type=${type}&categoryId=${categoryId}`,
});
} else {
window.RCHistory.push({
pathname: `/create-new-question?id=${id}&type=${type}`,
});
}
};
// 表头设置
parseColumns = () => {
const isPermiss = ["CloudManager", "StoreManager"].includes(
User.getUserRole()
);
const columns = [
{
title: "题目",
key: "questionStem",
dataIndex: "questionStem",
ellipsis: {
showTitle: false,
},
render: (val, record) => {
var handleVal = val;
handleVal = handleVal.replace(/<(?!img|input).*?>/g, "");
handleVal = handleVal.replace(/<\s?input[^>]*>/gi, "_、");
handleVal = handleVal.replace(/<\s?img[^>]*>/gi, "【图片】");
handleVal = handleVal.replace(/\&nbsp\;/gi, " ");
return (
<Tooltip
overlayClassName="tool-list"
title={
<div style={{ maxWidth: 700, width: "auto" }}>{handleVal}</div>
}
placement="topLeft"
overlayStyle={{ maxWidth: 700 }}
>
{handleVal}
</Tooltip>
);
},
},
{
title: "题型",
key: "questionTypeEnum",
dataIndex: "questionTypeEnum",
width: "16%",
render: (val) => {
return questionTypeEnum[val];
},
},
{
title: "正确率",
key: "accuracy",
dataIndex: "accuracy",
sorter: true,
showSorterTooltip: false,
width: "14%",
render: (val) => {
return val + "%";
},
},
{
title: "更新时间",
key: "updateTime",
dataIndex: "updateTime",
sorter: true,
showSorterTooltip: false,
width: "24%",
render: (val) => {
return formatDate("YYYY-MM-DD H:i:s", val);
},
},
{
title: "操作",
key: "operate",
dataIndex: "operate",
width: "24%",
render: (val, record) => {
return (
<div className="record-operate">
<div
className="record-operate__item"
onClick={() => this.previewQuestion(record.id)}
>
预览
</div>
{isPermiss && (
<span className="record-operate__item split"> | </span>
)}
{isPermiss && (
<div
className="record-operate__item"
onClick={() =>
this.toEditQuetion(record.id, record.questionTypeEnum)
}
>
编辑
</div>
)}
{isPermiss && (
<span className="record-operate__item split"> | </span>
)}
{isPermiss && (
<div
className="record-operate__item"
onClick={() => this.delCategoryConfirm(record)}
>
删除
</div>
)}
</div>
);
},
},
];
return columns;
};
// 自定义表格空状态
customizeRenderEmpty = () => {
const { categoryId } = this.state.query;
return (
<Empty
image="https://image.xiaomaiketang.com/xm/emptyTable.png"
imageStyle={{
height: 100,
}}
description={
<div>
<span>还没有题目</span>
{["CloudManager", "StoreManager"].includes(User.getUserRole()) &&
!["0", null].includes(categoryId) && (
<span>
,快去
<span
className="empty-list-tip"
onClick={() => {
this.handleCreateQuestionBank();
}}
>
新建一个
</span>
吧!
</span>
)}
</div>
}
></Empty>
);
};
onShowSizeChange = (current, size) => {
if (current == size) {
return;
}
let _query = this.state.query;
_query.size = size;
this.setState({ query: _query }, () => this.queryQuestionPageList());
};
// 改变搜索条件
handleChangeQuery = (searchType, value) => {
this.setState(
{
query: {
...this.state.query,
[searchType]: value || null,
current: 1,
},
},
() => {
if (searchType === "questionName") return;
this.queryQuestionPageList();
}
);
};
batchImportQuestion = () => {
const { categoryId } = this.state.query;
const ImportQuestionModal = (
<BatchImportQuestionModal
close={() => {
this.setState({ ImportQuestionModal: null }, () => {
this.queryQuestionPageList();
});
}}
categoryId={categoryId}
/>
);
this.setState({ ImportQuestionModal });
};
render() {
const { dataSource = [], total, query } = this.state;
const { current, size, categoryId, questionName, questionType } = query;
return (
<div className="question-manage-content">
<div className="question-manage-filter">
<Row type="flex" justify="space-between" align="top">
<div className="search-condition">
<div className="search-condition__item">
<span className="search-label">题目:</span>
<Search
placeholder="搜索题目名称"
value={questionName}
style={{ width: "calc(100% - 84px)" }}
onChange={(e) => {
this.handleChangeQuery("questionName", e.target.value);
}}
onSearch={() => {
this.queryQuestionPageList();
}}
enterButton={<span className="icon iconfont">&#xe832;</span>}
/>
</div>
<div className="search-condition__item">
<span className="search-label">题型:</span>
<Select
placeholder="请选择题目类型"
value={questionType}
style={{ width: "calc(100% - 70px)" }}
showSearch
allowClear
enterButton={<span className="icon iconfont">&#xe832;</span>}
filterOption={(inputVal, option) =>
option.props.children.includes(inputVal)
}
onChange={(value) => {
if (_.isEmpty(value)) {
this.handleChangeQuery("questionType", value);
}
}}
onSelect={(value) => {
this.handleChangeQuery("questionType", value);
}}
>
{_.map(questionTypeList, (item, index) => {
return (
<Select.Option value={item.value} key={item.key}>
{item.label}
</Select.Option>
);
})}
</Select>
</div>
</div>
<div className="reset-fold-area">
<Tooltip title="清空筛选">
<span
className="resetBtn iconfont icon"
onClick={this.handleReset}
>
&#xe61b;{" "}
</span>
</Tooltip>
</div>
</Row>
</div>
{["CloudManager", "StoreManager"].includes(User.getUserRole()) &&
!["0", null].includes(categoryId) && (
<Space size={16}>
<Button type="primary" onClick={this.handleCreateQuestionBank}>
新建题目
</Button>
<Button onClick={this.batchImportQuestion}>批量导入</Button>
</Space>
)}
<div className="question-manage-list">
<ConfigProvider renderEmpty={this.customizeRenderEmpty}>
<Table
rowKey={(record) => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
pagination={false}
bordered
onChange={this.handleChangeTable}
/>
</ConfigProvider>
{total > 0 && (
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={total}
toPage={(page) => {
const _query = { ...query, current: page + 1 };
this.setState({ query: _query }, () =>
this.queryQuestionPageList()
);
}}
showSizeChanger={true}
onShowSizeChange={this.onShowSizeChange}
/>
</div>
)}
{this.state.QuestionPreviewModal}
{this.state.ImportQuestionModal}
</div>
</div>
);
}
}
export default QuestionManageContent;
/*
* @Author: yuananting
* @Date: 2021-02-25 11:26:28
* @LastEditors: yuananting
* @LastEditTime: 2021-03-25 14:32:01
* @Description: 助学工具-题库-题目管理右侧内容样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-manage-content {
.question-manage-filter {
position: relative;
.search-condition {
width: calc(100% - 80px);
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
margin-bottom: 16px;
.search-label {
vertical-align: middle;
display: inline-block;
height: 32px;
line-height: 32px;
}
}
}
.reset-fold-area {
position: absolute;
right: 12px;
.resetBtn {
color: #999999;
font-size: 18px;
margin-right: 8px;
}
.fold-btn {
font-size: 14px;
color: #666666;
line-height: 20px;
.fold-icon {
font-size: 12px;
margin-left: 4px;
}
}
}
}
.data-icon {
cursor: pointer;
}
.question-manage-list {
position: relative;
margin-top: 16px;
.empty-list-tip {
color: #ffb714;
cursor: pointer;
}
.record-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-operate {
display: flex;
&__item {
color: #5289fa;
cursor: pointer;
&.split {
margin: 0 8px;
color: #bfbfbf;
}
}
}
}
}
.tool-list {
.ant-tooltip-inner {
max-width: 700px !important;
}
}
.fill-line {
padding: 0 10px;
border-bottom: 1px solid;
}
/*
* @Author: sunbingqing
* @Date: 2019-07-26 14:04:00
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-04-18 10:54:32
*/
import React, { useState, useEffect, useRef } from "react";
import "./XMAudio.less";
import VideoUpload from "@/core/upload";
import { string } from "prop-types";
// interface XMAudioProps {
// style?: any;
// index?: any;
// size?: number | string;
// url?: any;
// forbidParse?:boolean;
// getDuration?: (value: number) => void;
// }
let timerInterval;
const XMAudio = (props) => {
const ref = useRef();
const { style, size, getDuration } = props;
const [url, setUrl] = useState(props.url);
const [playing, setPlaying] = useState(false);
const [timer, setTimer] = useState(null);
const [playedTime, setPlayedTime] = useState(0);
const [leftTime, setLeftTime] = useState(Math.round(Number(size)));
const [totalTime, setTotalTime] = useState(Math.round(Number(size)));
const prevTimeRef = useRef();
useEffect(() => {
if(!props.forbidParse){
VideoUpload.getVideoParseRoute(props.url).then((newUrl) => {
setUrl(newUrl);
});
}
setLeftTime(Math.round(Number(size)));
setTotalTime(Math.round(Number(size)));
ref.current.addEventListener("pause", () => {
clearInterval(timer);
setPlaying(false);
setTimer(null);
if (ref.current && ref.current.ended) {
setLeftTime(totalTime);
setPlayedTime(0);
}
});
}, [props.url]);
useEffect(() => {
if (playing) {
timerInterval = setInterval(() => {
setPlayedTime((preTime) => {
prevTimeRef.current = preTime;
return prevTimeRef.current + 20;
});
if ((prevTimeRef.current + 20) % 1000 === 0) {
setLeftTime(totalTime - (prevTimeRef.current + 20));
}
if ((prevTimeRef.current + 30) >= totalTime) {
clearInterval(timerInterval);
setPlayedTime(0);
setLeftTime(totalTime);
setPlaying(false);
}
}, 20);
const audioDomList = document.querySelectorAll("audio");
for (let i = 0; i < Array.from(audioDomList).length; i++) {
if (audioDomList[i] === ref.current) {
ref.current.play();
setTimer(timerInterval)
} else {
audioDomList[i].pause();
}
}
} else {
ref.current.pause();
clearInterval(timer);
}
}, [playing]);
const audioImg = `https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/${
playing ? 1584514990496 : 1584514999661
}.png`;
function _togglePlay() {
playing ? pausePlay() : startPlay();
}
function pausePlay() {
setPlaying(false);
}
function startPlay() {
setPlaying(true);
}
function _changeTimeShow() {
let time = Math.floor(leftTime / 1000);
if (leftTime / 1000 > 60) {
const s = leftTime / 1000;
const second = Math.ceil(s % 60);
const minute = Math.floor(s / 60);
time = `${minute}'${second}`;
}
return time;
}
function _getDuration() {
const videoDiv = ref.current;
const videoSize = Math.round(videoDiv.duration) * 1000;
if (getDuration) {
setTotalTime(videoSize);
setLeftTime(videoSize);
getDuration(videoSize);
}
}
function _startTime() {
let time = Math.floor(playedTime / 1000);
let second = 0
let minute = 0;
let result = 0
if (time > 0) {
minute = Math.floor(time % 3600 / 60);
second = Math.floor((time - 60 * minute) % 60);
if (minute < 10) {
minute = "0" + minute;
}
if (second < 10) {
second = "0" + second;
}
result = minute + ':' + second
}else{
result = "00:00"
}
return result;
}
function _endTime() {
let time = Math.floor(totalTime / 1000);
let second = 0
let minute = 0;
let result = 0
if (time > 0) {
minute = Math.floor(time % 3600 / 60);
second = Math.floor((time - 60 * minute) % 60);
if (minute < 10) {
minute = "0" + minute;
}
if (second < 10) {
second = "0" + second;
}
result = minute + ':' + second
}
if(time === 0){
result = "00:00"
}
return result;
}
function putDownFlag(event) {
let dragDiv = event.target;
dragDiv.style.cursor = "pointer";
let offsetX = parseInt(dragDiv.style.left); // 获取当前的x轴距离
let innerX = event.clientX - offsetX; // 获取鼠标在方块内的x轴距
// 按住鼠标时为div修改样式
dragDiv.style.width = "16px";
dragDiv.style.height = "16px";
dragDiv.style.top = "-7px";
dragDiv.style.backGround = "linear-gradient(180deg, #FFB467 0%, #FF9143 100%)"
// 鼠标移动的时候不停的修改div的left和top值
document.onmousemove = function (event) {
dragDiv.style.left = event.clientX - innerX + "px";
// 边界判断
if (parseInt(dragDiv.style.left) <= 0) {
dragDiv.style.left = "0px";
}
if (parseInt(dragDiv.style.left) >= 154) {
dragDiv.style.left = "149px";
}
setPlayedTime(parseInt(dragDiv.style.left) / 150 * totalTime)
}
// 鼠标抬起时,清除绑定在文档上的mousemove和mouseup事件
// 否则鼠标抬起后还可以继续拖拽方块
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
// 清除border
dragDiv.style.top = "-4px";
dragDiv.style.width = "10px";
dragDiv.style.height = "10px";
}
}
function mouseOver(event){
let dragDiv = event.target;
dragDiv.style.cursor = "pointer";
dragDiv.style.width = "16px";
dragDiv.style.height = "16px";
dragDiv.style.top = "-7px";
dragDiv.style.backGround = "linear-gradient(180deg, #FFB467 0%, #FF9143 100%)"
}
function mouseLeave (event){
let dragDiv = event.target;
dragDiv.style.top = "-4px";
dragDiv.style.width = "10px";
dragDiv.style.height = "10px";
}
return (
<div className="xm-audio" style={style}>
<img src={audioImg} onClick={_togglePlay} className="audio-image" />
<div className="process-area">
<div
className="complete-area"
style={{ width: `${(playedTime / totalTime) * 150}px ` }}
/>
<div
className="flag"
style={{ left: `${(playedTime / totalTime) * 150}px ` }}
onMouseDown={(e) => putDownFlag(e)}
onMouseOver={(e)=> mouseOver(e)}
onMouseLeave={(e)=>mouseLeave(e)}
/>
</div>
<span className="sec-time">
{`${_startTime()}`}/{`${_endTime()}`}
</span>
<audio
src={url}
ref={ref}
autoPlay={false}
onCanPlayThrough={_getDuration}
/>
</div>
);
};
export default XMAudio;
/*
* @Author: Michael
* @Date: 2018-01-31 14:55:14
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-03-24 10:18:43
*/
.xm-audio {
display: flex;
align-items: center;
width: 280px;
border-radius: 5px;
background-color: #ffffff;
.audio-image {
width: 28px !important;
height: 28px;
margin-left: 0px !important;
cursor: pointer;
}
.icon {
margin-left: 10px;
font-size: 25px;
margin-right: 15px;
cursor: pointer;
}
.play-icon {
color: #FC8540;
}
.sec-time{
white-space: nowrap;
color: #FF8534;
margin-left: 12px;
font-size: 13px;
}
.process-area {
height: 4px;
width: 154px;
border-radius: 3px;
margin-left: 12px;
position: relative;
background:rgba(255,133,52,0.2);
.complete-area {
height: 100%;
background-color: #FF8534;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.flag {
position: absolute;
top: -4px;
width: 10px;
height: 10px;
border-radius: 50%;
background:linear-gradient(180deg,rgba(255,180,103,1) 0%,rgba(255,145,67,1) 100%);
}
}
}
/*
* @Author: 吴文洁
* @Date: 2020-03-18 10:01:28
* @LastEditors: yuananting
* @LastEditTime: 2021-03-22 17:24:38
* @Description: 录音组件
*/
import React, { Component } from "react";
import { Button, Modal } from "antd";
import UploadOss from "@/core/upload";
import { RECORD_ERROR } from "@/common/constants/academic";
import AudioRecorder from "./audioRecord";
import "./XMRecord.less";
class XMRecord extends Component {
constructor(props) {
super(props);
//从麦克风获取的音频流
this.mAudioContext = null;
this.mAudioFromMicrophone = null;
this.mMediaRecorder = null;
this.mChunks = [];
this.state = {
isFinished: true,
recordTime: 0,
};
}
componentDidMount() {
// 获取录音设备
this.getAudioRecorderDevice();
}
componentWillUnmount() {}
getAudioRecorderDevice = () => {
//仅用来进行录音
const constraints = { audio: true };
// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function (constraints) {
// 首先,如果有getUserMedia的话,就获得它
var getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia;
// 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
if (!getUserMedia) {
return Promise.reject(
new Error("getUserMedia is not implemented in this browser")
);
}
// 否则,为老的navigator.getUserMedia方法包裹一个Promise
return new Promise(function (resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
};
openDeviceFailure = (reason) => {
const { title = "麦克风调用错误", content = "请检查麦克风是否可用" } =
RECORD_ERROR[reason.name] || {};
Modal.info({
title,
content,
okText: "我知道了",
});
};
handleCloseConfirm = () => {
this.$confirm = null;
this.forceUpdate();
};
handleStartRecord = () => {
navigator.mediaDevices
.getUserMedia({
audio: true,
})
.then((stream) => {
this.mMediaRecorder = new AudioRecorder(stream);
this.mMediaRecorder.start();
this.handleCountTime();
}, this.openDeviceFailure);
};
onProcessData = (audioData) => {
this.mChunks.push(audioData.data);
};
handleCountTime = () => {
const { maxTime = 180 } = this.props;
this.setState({ isFinished: false });
// 开始计时
this.timer = window.setInterval(() => {
const { recordTime } = this.state;
if (recordTime > maxTime - 1) {
window.clearInterval(this.timer);
this.handleFinishRecord();
return;
}
this.setState({
recordTime: this.state.recordTime + 1,
});
}, 1000);
};
handleFinishRecord = () => {
if (this.mMediaRecorder) {
this.mMediaRecorder.stop();
}
const blob = this.mMediaRecorder.upload();
UploadOss.uploadBlobToOSS(blob, window.random_string(16) + ".wav").then(
(mp3URL) => {
const { recordTime } = this.state;
this.props.onFinish(mp3URL, recordTime * 1000);
this.setState({
recordTime: 0,
isFinished: true,
});
window.clearInterval(this.timer);
}
);
};
// 格式化当前录音时长
formatRecordTime = (timeStamp) => {
let minutes = Math.floor(timeStamp / 60);
let seconds = timeStamp % 60;
minutes = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds;
return `${minutes}:${seconds}`;
};
handleCancel = () => {
if (this.mMediaRecorder) {
this.mMediaRecorder.stop();
}
window.clearInterval(this.timer);
this.setState(
{
recordTime: 0,
isFinished: true,
},
() => {
this.props.onCancel();
}
);
};
render() {
const { isFinished, recordTime } = this.state;
const { visible, maxTime = 180 } = this.props;
return (
<div className={`xm-record ${visible ? "visible" : "hidden"}`}>
<div className="record-left">
<div className="text">
{isFinished ? (
<img
style={{ width: 14, height: 14 }}
src="https://image.xiaomaiketang.com/xm/xCahGm2Q2J.png"
/>
) : (
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1584607726775.png" />
)}
{isFinished ? <span>录音</span> : <span>录音中...</span>}
</div>
<div className="time">{`${this.formatRecordTime(
recordTime
)}/${this.formatRecordTime(maxTime)}`}</div>
</div>
<div className="btn-wrapper">
<Button onClick={this.handleCancel}>取消</Button>
{isFinished ? (
<Button type="primary" onClick={this.handleStartRecord}>
开始
</Button>
) : (
<Button
type="primary"
className="finish"
onClick={this.handleFinishRecord}
>
完成
</Button>
)}
</div>
</div>
);
}
}
export default XMRecord;
.xm-record {
position: fixed;
bottom: -100px;
left: 50%;
transform: translate(-50%, 0);
height: 56px;
width: 336px;
border-radius: 4px;
background: #FFF;
box-shadow: 0 2px 10px 0 rgba(0,0,0,.05);
margin-bottom: 32px;
transition: bottom 0.5s;
.record-left{
width: 100px;
height: 56px;
display: flex;
flex-direction: column;
margin-left: 24px;
align-items: flex-start;
justify-content: center;
}
&.visible {
animation: visibleRecord 0.5s;
bottom: 4px;
z-index: 10000;
}
&.hidden {
animation: hiddenRecord 0.5s;
bottom: -100px;
}
@keyframes visibleRecord {
from{
display: none;
}
to{
display: block;
}
}
@keyframes hiddenRecord {
from{
display: block;
}
to{
display: none;
}
}
.text {
height: 25px;
img {
margin-right: 3px;
}
}
.btn-wrapper {
position: absolute;
right: 24px;
top: 9px;
.ant-btn {
margin-left: 12px;
&.finish {
background-color: #FF483C !important;
}
}
}
}
\ No newline at end of file
export default function AudioRecorder(stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 16; //采样数位 8, 16
config.sampleRate = config.sampleRate || 16000; //采样率16khz
var context = new (window.webkitAudioContext || window.AudioContext)();
var audioInput = context.createMediaStreamSource(stream);
var createScript = context.createScriptProcessor || context.createJavaScriptNode;
var recorder = createScript.apply(context, [4096, 1, 1]);
var audioData = {
size: 0 //录音文件长度
, buffer: [] //录音缓存
, inputSampleRate: context.sampleRate //输入采样率
, inputSampleBits: 16 //输入采样数位 8, 16
, outputSampleRate: config.sampleRate //输出采样率
, oututSampleBits: config.sampleBits //输出采样数位 8, 16
, input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
}
, compress: function () { //合并压缩
//合并
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//压缩
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0, j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
}
, encodeWAV: function () {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1;//单声道
var offset = 0;
var writeString = function (str) {
for (var i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
}
// 资源交换文件标识符
writeString('RIFF'); offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + dataLength, true); offset += 4;
// WAV文件标志
writeString('WAVE'); offset += 4;
// 波形格式标志
writeString('fmt '); offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true); offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true); offset += 2;
// 通道数
data.setUint16(offset, channelCount, true); offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true); offset += 4;
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
// 每样本数据位数
data.setUint16(offset, sampleBits, true); offset += 2;
// 数据标识符
writeString('data'); offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, dataLength, true); offset += 4;
// 写入采样数据
if (sampleBits === 8) {
for (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
};
//开始录音
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
}
//停止
this.stop = function () {
stream.getTracks().forEach(function(track) {
track.stop();
});
recorder.disconnect();
}
//获取音频文件
this.getBlob = function () {
this.stop();
return audioData.encodeWAV();
}
//回放
this.play = function (audio) {
var blob=this.getBlob();
// saveAs(blob, "F:/3.wav");
audio.src = window.URL.createObjectURL(this.getBlob());
}
//上传
this.upload = function () {
return this.getBlob()
}
//音频采集
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
//record(e.inputBuffer.getChannelData(0));
}
}
\ No newline at end of file
/*
* @Author: chenjianyu
* @Date: 2020-09-12 17:00:44
* @LastEditTime: 2021-03-18 17:23:09
* @LastEditors: sunbingqing
* @Description: 答题模式模板
* @Copyright © 2020 杭州杰竞科技有限公司 版权所有
*/
export function defineModuleData(index) {
return {
questionInfoVOList: [], // 题目内容
taskModuleContentVOList: [], // 模块说明内容
taskModuleInfoVO: {
key: window.random_string(16),
name: `题目模块${index}`,
}
}
}
export function defineQuestionData(questionType) {
return {
taskModuleQuestionVO: { // 题目信息
key: window.random_string(16),
questionType, // 题目类型
itemTypeEnum: 'RIGHT_OR_WRONG',
questionLevel: 0,
questionTypeEnum: questionType,
// score: 1, // 本题分值
},
itemInfoVOList: [], // 选项
topicDescribeVOList: [{
content: '',
type: 'TEXT',
contentType: 'TEXT'
}], // 题干
parsingDescribeVOList: [{
content: '',
type: 'TEXT',
contentType: 'TEXT'
}], // 答案解析
lowerLevelQuestionInfoVOList: [],
showBox: true,
}
}
export function defineQuestionInfo(questionType) {
return {
questionTypeEnum: questionType, // 题目类型
questionStemList:[
{
contentType: "QUESTION_STEM", // 内容类型(默认题干)
content: "", // 内容
type: "RICH_TEXT", // 内容项形式(0:富文本 1:文字 2:图片 3:语音 4:视频 5文件 6.课件)
}
], // 题干
optionList: questionType === "JUDGE" ? [] : [], // 非填空题选项
gapFillingAnswerList: [
// {
// correctAnswerList: []
// }
], //填空题填空项
questionAnswerDescList: [
{
contentType: "QUESTION_ANSWER_DESC", // 内容类型(默认解析)
content: "", // 内容
type: "RICH_TEXT", // 内容项形式(0:富文本 1:文字 2:图片 3:语音 4:视频 5文件 6.课件)
}
], //答案解析
showBox: true, // 显示录音弹窗
}
}
export function defineOptionInfo() {
return {
isCorrectAnswer: 0, // 是否为正确答案选项
questionOptionContentList: [ // 选项内容
{
contentType: "QUESTION_OPTION", // 内容类型(默认选项)
content: "", // 内容
type: "RICH_TEXT", // 内容项形式(0:富文本 1:文字 2:图片 3:语音 4:视频 5文件 6.课件)
}
]
}
}
export function defineJudgeOptionInfo(content) {
return {
isCorrectAnswer: 0, // 是否为正确答案选项
questionOptionContentList: [ // 选项内容
{
contentType: "QUESTION_OPTION", // 内容类型(默认选项)
content, // 内容
type: "RICH_TEXT",
}
]
}
}
export function defineOptionData(content = '') {
return {
itemContentVOList: [{
content,
type: 'TEXT',
contentType: 'TEXT'
}], // 题目项内容
questionItemVO: { // 题目项信息
ifCorrectAnswerItem: false,
key: window.random_string(16),
}
}
}
\ No newline at end of file
/*
* @Author: zhangyi
* @Date: 2019-12-09 10:29:55
* @Last Modified by: mikey.wanghaofeng
* @Last Modified time: 2020-09-25 11:03:47
*/
import React, { Component } from "react";
import { Modal, Button, Upload, message, Spin, Progress } from "antd";
import "./BatchImportQuestionModal.less";
import SelectPrepareFileModal from "@/modules/prepare-lesson/modal/SelectPrepareFileModal";
import User from "@/common/js/user";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
import { LoadingOutlined } from "@ant-design/icons";
class BatchImportQuestionModal extends Component {
constructor(props) {
super(props);
this.state = {
showSelectFileModal: false, // 云盘列表弹窗显隐
diskList: [], // 资料云盘文件列表
uploadFile: null, // 上传的文件
uploadResult: {
elapsedTime: 0,
failCnt: 0,
resourceUrl: null,
successCnt: 0,
}, // 上传返回结果
message: null,
status: "init",
};
}
// 下载题目模板
handleDownTemplate = () => {
const a = document.createElement("a");
a.href = "https://image.xiaomaiketang.com/xm/题目批量导入标准模版.xlsx";
a.click();
};
// 选择云盘资源
handleSelectExcel = (file) => {
this.setState({ uploadFile: file });
this.setState({
showSelectFileModal: false,
});
};
// 导入
handleImport = async () => {
const { uploadFile } = this.state;
if (!uploadFile) {
message.warning("请选择要导入的文件");
} else {
let flag = false;
setTimeout(() => {
if (!flag) {
this.setState({ status: "uploading" });
}
}, 1000);
let params = {
categoryId: this.props.categoryId,
resourceId: this.state.uploadFile.resourceId,
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
const res = await QuestionBankService.batchImport(params);
const { result } = res;
const { bizSuccess, bizMessage } = result;
if (res) {
flag = true;
this.setState({ status: bizSuccess ? "success" : "fail" });
this.setState({ uploadResult: result });
this.setState({ message: bizMessage });
}
}
};
// 下载错误报告
downErrorReport = async () => {
const { uploadResult } = this.state;
const { resourceUrl } = uploadResult;
const a = document.createElement("a");
a.href = resourceUrl;
a.click();
};
render() {
const {
diskList,
showSelectFileModal,
uploadFile,
status,
uploadResult,
message,
} = this.state;
const { failCnt, resourceUrl, successCnt } = uploadResult;
const loadingIcon = <LoadingOutlined style={{ fontSize: 76 }} spin />;
return (
<div>
<Modal
closable={status !== "uploading"}
className="import-score-modal"
title="导入题目信息"
visible={true}
width={560}
maskClosable={false}
footer={
status === "init" && [
<Button onClick={this.props.close}>取消</Button>,
<Button type="primary" onClick={this.handleImport}>
导入
</Button>,
]
}
onCancel={() => {
this.props.close();
}}
>
{status === "init" && (
<div>
<div className="step-section">
<h4 className="step-title">1.下载导入模板,按要求填写信息</h4>
<div
className="down-btn"
style={{ fontSize: "14px" }}
onClick={this.handleDownTemplate}
>
下载题目导入模板
</div>
</div>
<div className="step-section">
<h4 className="step-title">2.选择需要导入的Excel文件</h4>
<p style={{ marginBottom: 16, color: "gray" }}>
导入限制:一次最多导入1000个题目
</p>
<Button
type="primary"
className="add-btn"
onClick={() => this.setState({ showSelectFileModal: true })}
>
{uploadFile ? "重新添加" : "添加文件"}
</Button>
{uploadFile && (
<div className="file-box">
<div>
<img
className="link-img"
src="https://image.xiaomaiketang.com/xm/link.png"
/>
<span>{uploadFile.folderName}</span>
<img
className="del-img"
src="https://image.xiaomaiketang.com/xm/del.png"
onClick={() => this.setState({ uploadFile: null })}
/>
</div>
</div>
)}
</div>
</div>
)}
{status === "uploading" && (
<div className="import-status-box">
<div className="status-content">
<Spin indicator={loadingIcon} />
<p className="status">题目导入中...</p>
<p className="status-tip">请勿关闭页面和此弹窗</p>
<p className="status-tip">
<span>以防题目导入失败</span>
</p>
</div>
</div>
)}
{status === "success" && (
<div className="import-status-box">
<div className="status-content">
<img
src="https://image.xiaomaiketang.com/xm/success.png"
alt=""
/>
<p className="status">导入完成</p>
<p className="status-tip">
<span style={{ color: successCnt === 0 ? "" : "#FF9D14" }}>
{successCnt}
</span>
个题目导入成功,
<span style={{ color: failCnt === 0 ? "" : "#FF9D14" }}>
{failCnt}
</span>
个题目导入失败。
</p>
{resourceUrl ? (
<Button
type="primary"
className="down-btn"
onClick={() => this.downErrorReport(resourceUrl)}
>
下载错误报告
</Button>
) : (
<Button
onClick={() => {
this.props.close();
}}
>
关闭
</Button>
)}
</div>
</div>
)}
{status === "fail" && (
<div className="import-status-box">
<div className="status-content">
<img src="https://image.xiaomaiketang.com/xm/fail.png" alt="" />
<p className="status">导入失败</p>
<p className="status-tip">{message}</p>
{message ? (
<Button
type="primary"
className="down-btn"
onClick={() => {
this.setState({ status: "init" });
this.setState({ uploadFile: null });
this.setState({ showSelectFileModal: true });
}}
>
重新上传文件
</Button>
) : (
<Button
onClick={() => {
this.props.close();
}}
>
关闭
</Button>
)}
</div>
</div>
)}
</Modal>
<SelectPrepareFileModal
operateType="select"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
selectTypeList={[
"vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"vnd.ms-excel",
]}
tooltip="支持文件类型:xls、xlsx"
isOpen={showSelectFileModal}
diskList={diskList}
onClose={() => {
this.setState({ showSelectFileModal: false });
}}
onSelect={this.handleSelectExcel}
/>
</div>
);
}
}
export default BatchImportQuestionModal;
@import '../../../core/mixins.less';
.import-score-modal {
.step-section {
margin-bottom: 24px;
.step-title {
font-size: 16px;
font-weight: 400;
color: #333;
}
.tip-box {
border: 1px dashed #e8e8e8;
padding: 8px;
margin-bottom: 16px;
.tip-title {
margin-bottom: 4px;
}
.tip-content {
font-size: 12px;
}
}
.add-btn {
border-radius: 2px;
font-size: 14px;
box-shadow: none;
text-shadow: none;
span {
font-weight: 400;
}
}
.file-box {
line-height: 20px;
color: #999999;
line-height: 20px;
margin-top: 10px;
span {
vertical-align: middle;
margin-right: 16px;
}
.link-img {
width: 14px;
vertical-align: middle;
margin-right: 4px;
}
.del-img {
width: 18px;
vertical-align: middle;
visibility: hidden;
}
}
.file-box :hover {
background-color: #FFF8E8;
.del-img {
visibility: visible !important;
}
}
.remark-input {
width: 304px;
margin-bottom: 8px;
}
.remark-tip {
color: #999;
font-size: 12px;
}
.down-btn {
text-align: left;
color: #5289FA;
font-size: 12px;
display: block;
margin-top: 8px;
cursor: pointer;
}
.upload-box {
width: 200px;
.ant-upload-list-item-name {
.text-overflow-ellipsis();
width:70%;
}
}
}
.import-status-box {
height:430px;
overflow: hidden;
.status-content {
margin:auto;
text-align: center;
margin-top:100px;
>img {
width: 76px;
}
.status {
font-size: 16px;
font-weight: 500;
margin: 18px 0 16px;
}
.status-tip {
line-height: 20px;
color: #666666;
margin-bottom: 16px;
.num {
color: #FC9C6B;
}
}
}
}
}
\ No newline at end of file
/*
* @Author: yuananting
* @Date: 2021-02-22 17:51:28
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 15:02:53
* @Description: 助学工具-题库-题库新建或编辑题库分类模态框
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Modal, Form, Input, message } from "antd";
import User from "@/common/js/user";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
class NewEditQuestionBankCategory extends Component {
formRef = React.createRef();
constructor(props) {
super(props);
this.state = {
treeData: [],
categoryName:
this.props.type === "edit" ? this.props.node.categoryName : null,
};
}
componentDidMount() {
// document.getElementById("categoryName").setAttribute("style", "autocomplete","off")
this.queryCategoryTree();
}
// 查询分类树
queryCategoryTree = () => {
let query = {
source: 0,
userId: User.getStoreUserId(),
tenantId: User.getStoreId(),
};
QuestionBankService.queryCategoryTree(query).then((res) => {
const { result = [] } = res;
this.setState({ treeData: result });
});
};
// 确定新增或编辑
confirmOperate = async () => {
const { categoryName } = this.state;
const { node, addLevelType, type } = this.props;
let params = {
source: 0,
tenantId: User.getStoreId(),
userId: User.getStoreUserId(),
};
if (type === "new") {
//新增
params.categoryName = categoryName;
if (addLevelType === "equal") {
params.parentId = node ? node.parentId : 0
params.categoryLevel = node ? node.categoryLevel : 0;
} else {
params.parentId = node.id;
params.categoryLevel = node.categoryLevel + 1;
}
try {
await this.formRef.current.validateFields();
QuestionBankService.addCategory(params).then((res) => {
if (res.success) {
this.props.close();
}
});
} catch (e) {
console.log(e);
}
} else {
// 编辑
params.categoryId = node.id;
params.parentId = node.parentId;
params.categoryLevel = node.categoryLevel;
params.categoryName = categoryName;
try {
await this.formRef.current.validateFields();
QuestionBankService.editCategory(params).then((res) => {
if (res.success) {
this.props.close();
}
});
} catch (e) {
console.log(e);
}
}
};
getEqualLevelNodes = (data, parentId) => {
let nodes = [];
data.forEach((item) => {
if (item.parentId === parentId) {
nodes.push(item);
}
if (item.children) {
nodes.push(...this.getEqualLevelNodes(item.children, parentId));
}
});
return nodes;
};
getChildLevelNodes = (data, id) => {
let nodes = [];
data.forEach((item) => {
if (item.id === id && item.children) {
nodes.push(...item.children);
}
if (item.children) {
nodes.push(...this.getChildLevelNodes(item.children, id));
}
});
return nodes;
};
getSameLevelNodes = (data, type) => {
let sameLevelNodes = [];
if (type === "equal") {
let parentId = this.props.node ? this.props.node.parentId : "0";
sameLevelNodes = this.getEqualLevelNodes(data, parentId);
} else {
sameLevelNodes = this.getChildLevelNodes(data, this.props.node.id);
}
return sameLevelNodes;
};
// 查询是否重名
checkExist = (sameLevelNodes, categoryName) => {
if ((sameLevelNodes.length > 0 && sameLevelNodes[0].parentId === "0")) {
if (categoryName === "未分类") {
return true;
}
}
var result = null;
sameLevelNodes.forEach((item) => {
if (result != null) {
return result;
}
if (item.categoryName === categoryName) {
result = item;
}
});
return result;
};
render() {
const { title, label, treeData, addLevelType } = this.props;
const { categoryName } = this.state;
const _that = this;
return (
<Modal
visible={true}
title={title}
onOk={this.confirmOperate}
onCancel={() => this.props.close()}
>
<Form ref={this.formRef}>
<Form.Item
name="categoryName"
label={label}
required
rules={[
{
required: true,
message: `请输入${label}`,
},
({ getFieldValue }) => ({
validator(_, value) {
let sameLevelNodes = _that.getSameLevelNodes(
treeData,
addLevelType
);
if (_that.checkExist(sameLevelNodes, value)) {
return Promise.reject("此分类名称已存在");
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input
defaultValue={categoryName}
placeholder={`请输入${title},最多10个字`}
maxLength={10}
autoComplete="off"
onChange={(e) => {
this.setState({
categoryName: e.target.value,
});
}}
/>
</Form.Item>
</Form>
</Modal>
);
}
}
export default NewEditQuestionBankCategory;
import React, { Component } from "react";
import { Modal, Divider } from "antd";
import User from "@/common/js/user";
import QuestionBankService from "@/domains/question-bank-domain/QuestionBankService";
import "./QuestionPreviewModal.less";
import ScanFileModal from "@/modules/resource-disk/modal/ScanFileModal";
import _ from "underscore";
import XMAudio from "../components/XMAudio";
import { NUM_TO_WORD_MAP } from "@/common/constants/punchClock/punchClock";
const questionTypeList = {
SINGLE_CHOICE: "单选题",
MULTI_CHOICE: "多选题",
JUDGE: "判断题",
GAP_FILLING: "填空题",
INDEFINITE_CHOICE: "不定项选择题",
};
class QuestionPreviewModal extends Component {
formRef = React.createRef();
constructor(props) {
super(props);
this.state = {
questionInfo: {},
};
}
componentDidMount() {
this.queryQuestionDetails();
}
// 题目预览
queryQuestionDetails = () => {
let query = {
id: this.props.id,
source: 0,
userId: User.getStoreUserId(),
tenantId: User.getStoreId(),
};
QuestionBankService.queryQuestionDetails(query).then((res) => {
const { result = [] } = res;
this.setState({ questionInfo: result });
});
};
handleScanFile = (scanFileType, scanFileAddress) => {
this.setState({
showScanFile: true,
scanFileAddress,
scanFileType,
});
};
transferStemDocument = (txt) => {
const template = `<p class='content'>${txt}</p>`;
let doc = new DOMParser().parseFromString(template, "text/html");
let p = doc.querySelector(".content");
return p;
};
render() {
const {
showScanFile,
scanFileAddress,
scanFileType,
questionInfo,
} = this.state;
const {
questionTypeEnum,
questionStemList,
gapFillingAnswerList,
optionList,
questionAnswerDescList,
} = questionInfo;
// 查找答案选项
let rightAnswerSort = [];
_.filter(optionList, (optionItem, optionIndex) => {
if (optionItem.isCorrectAnswer === 1) {
rightAnswerSort.push(optionIndex);
}
});
const textDescList = _.filter(questionAnswerDescList, (descItem) => {
return descItem.type === "RICH_TEXT";
});
const pictureDescList = _.filter(questionAnswerDescList, (descItem) => {
return descItem.type === "PICTURE";
});
const voiceDescList = _.filter(questionAnswerDescList, (descItem) => {
return descItem.type === "VOICE";
});
const audioDescList = _.filter(questionAnswerDescList, (descItem) => {
return descItem.type === "AUDIO";
});
const videoDeacList = _.filter(questionAnswerDescList, (descItem) => {
return descItem.type === "VIDEO";
});
return (
<div>
<Modal
className="question-preview-modal"
visible={true}
title="题目预览"
width={560}
centered={true}
footer={null}
onCancel={this.props.close}
>
<div className="question-modal-content">
<div className="question-type">
<div className="question-type__title">题型:</div>
<div className="question-type__content">
{questionTypeList[questionTypeEnum]}
</div>
</div>
<div className="question-stem">
<div className="question-stem__title">题目:</div>
<div className="question-stem__content">
{_.map(questionStemList, (item, index) => {
let dom = "";
let { type, content, size } = item;
switch (type) {
case "RICH_TEXT":
if (questionTypeEnum === "GAP_FILLING") {
content = content.replace(
/_/g,
`<input
class="add-fill-line"
disabled
correctAnswerList=""
id=${window.random_string(16)}
value="填空"
/>`
);
}
dom = (
<div
key={index}
className="input-box"
dangerouslySetInnerHTML={{
__html: content,
}}
/>
);
break;
case "PICTURE":
dom = (
<div key={index} className="picture-box">
<img
src={content}
onClick={() => this.handleScanFile("JPG", content)}
/>
</div>
);
break;
case "VOICE":
dom = (
<div key={index} className="voice-box">
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={index}
size={size || 1000}
/>
</div>
);
break;
case "AUDIO":
dom = (
<div key={index} className="voice-box">
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={index}
size={size || 1000}
/>
</div>
);
break;
}
return dom;
})}
</div>
</div>
{[
"INDEFINITE_CHOICE",
"MULTI_CHOICE",
"SINGLE_CHOICE",
"JUDGE",
].includes(questionTypeEnum) && (
<div className="question-option">
<div className="question-option__title">选项:</div>
<div className="question-option__content">
{_.map(optionList, (optionItem, optionIndex) => {
const { questionOptionContentList } = optionItem;
const inputcontent = _.filter(
questionOptionContentList,
(optionItem) => {
return optionItem.type === "RICH_TEXT";
}
);
return (
<div className="option-box" key={optionIndex}>
<div className="option-box-header">
<div className="option-sort">
{NUM_TO_WORD_MAP[optionIndex]}.
</div>
{[
"INDEFINITE_CHOICE",
"MULTI_CHOICE",
"SINGLE_CHOICE",
].includes(questionTypeEnum) && (
<div
className="input-box"
dangerouslySetInnerHTML={{
__html: inputcontent[0].content,
}}
/>
)}
{["JUDGE"].includes(questionTypeEnum) &&
_.map(questionOptionContentList, (item, index) => {
item.content = item.content.replace(/<\/?[^>]*>/g, "");
return <span key={index}>{item.content}</span>;
})}
</div>
{[
"INDEFINITE_CHOICE",
"MULTI_CHOICE",
"SINGLE_CHOICE",
].includes(questionTypeEnum) &&
_.map(questionOptionContentList, (item, index) => {
let dom = "";
let { type, content, size } = item;
switch (type) {
case "PICTURE":
dom = (
<div key={index + 1} className="picture-box">
<img
src={content}
onClick={() =>
this.handleScanFile("JPG", content)
}
/>
</div>
);
break;
case "VOICE":
dom = (
<div key={index + 1} className="voice-box">
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={index}
size={size || 1000}
/>
</div>
);
break;
case "AUDIO":
dom = (
<div key={index} className="voice-box">
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={index}
size={size || 1000}
/>
</div>
);
break;
}
return dom;
})}
</div>
);
})}
</div>
</div>
)}
<div className="question-answer">
<div className="question-answer__title">答案:</div>
{[
"INDEFINITE_CHOICE",
"MULTI_CHOICE",
"SINGLE_CHOICE",
"JUDGE",
].includes(questionTypeEnum) && (
<div className="question-answer__content">
{_.map(rightAnswerSort, (item, index) => {
return (
<div className="option-sort" key={index}>
{NUM_TO_WORD_MAP[item]}
</div>
);
})}
</div>
)}
{questionTypeEnum === "GAP_FILLING" && (
<div className="question-gap-answer">
{_.map(gapFillingAnswerList, (item, index) => {
return (
<div>
<div className="gap-label">填空{index + 1}.</div>
<div className="gap-content" key={index}>
{_.map(
item.correctAnswerList,
(childItem, childIndex) => {
return <span>{childItem}</span>;
}
)}
</div>
</div>
);
})}
</div>
)}
</div>
<div className="question-desc">
<div className="question-desc__title">答案解析:</div>
<div className="question-desc__content">
<div className="question-desc-box">
{textDescList.length > 0 && (
<div
className="desc-input-box"
dangerouslySetInnerHTML={{
__html: textDescList[0].content,
}}
/>
)}
{pictureDescList.length > 0 && (
<div className="desc-picture-box">
{_.map(pictureDescList, (pictureItem, pictureIndex) => {
let { content } = pictureItem;
return (
<div className="picture-box" key={pictureIndex}>
<img
className="img-box"
src={content}
onClick={() =>
this.handleScanFile("JPG", content)
}
/>
</div>
);
})}
</div>
)}
{audioDescList.length > 0 && (
<div className="desc-audio-box">
{_.map(audioDescList, (audioItem, audioIndex) => {
let { content, size } = audioItem;
return (
<div className="audio-box" key={audioIndex}>
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={audioIndex}
size={size || 1000}
/>
</div>
);
})}
</div>
)}
{voiceDescList.length > 0 && (
<div className="desc-audio-box">
{_.map(voiceDescList, (voiceItem, voiceIndex) => {
let { content, size } = voiceItem;
return (
<div className="audio-box" key={voiceIndex}>
<XMAudio
forbidParse
url={content}
getDuration={(durationSize) => {
size = durationSize;
this.setState({});
}}
index={voiceIndex}
size={size || 1000}
/>
</div>
);
})}
</div>
)}
{videoDeacList.length > 0 && (
<div className="desc-video-box">
{_.map(videoDeacList, (videoItem, videoIndex) => {
let { content } = videoItem;
return (
<div className="video-box" key={videoIndex}>
<img
className="video-box_content"
src={`${content}?x-oss-process=video/snapshot,t_0,m_fast`}
/>
<img
className="video-box_btn"
src="https://image.xiaomaiketang.com/xm/r5H8cYm4ch.png"
onClick={() =>
this.handleScanFile("MP4", content)
}
/>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</div>
</Modal>
{showScanFile && (
<ScanFileModal
modalTitle={scanFileType === "MP4" ? "视频播放" : "查看大图"}
fileType={scanFileType}
item={{
ossAddress: scanFileAddress,
}}
close={() => {
this.setState({ showScanFile: false });
}}
/>
)}
</div>
);
}
}
export default QuestionPreviewModal;
.question-modal-content {
position: relative;
.question-type {
margin-bottom: 16px;
&__title {
height: 22px;
font-size: 16px;
color: #333333;
line-height: 22px;
margin-bottom: 8px;
}
&__content {
font-size: 14px;
font-weight: 400;
color: #666666;
}
}
.question-stem {
margin-bottom: 16px;
border-bottom: 1px solid #E8E8E8;
padding-bottom: 16px;
&__title {
height: 22px;
font-size: 16px;
color: #333333;
line-height: 22px;
margin-bottom: 8px;
}
&__content {
font-size: 14px;
font-weight: 400;
color: #666666;
.input-box {
margin-bottom: 8px;
* {
display: inline-block;
}
}
.picture-box {
width: 88px;
height: 88px;
border-radius: 4px;
overflow: hidden;
align-items: center;
justify-content: center;
margin-right: 12px;
position: relative;
display: inline-flex;
border: 1px solid #e8e8e8;
img {
max-width: 100%;
max-height: 100%;
border-radius: 4px;
vertical-align: middle;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.voice-box {
margin-bottom: 12px;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
width: 320px;
}
}
.add-fill-line {
padding: 0 10px;
border-bottom: 1px solid !important;
margin: 0 4px;
text-align: center;
border: none;
width: 54px;
}
}
.question-option {
margin-bottom: 16px;
&__title {
height: 22px;
font-size: 16px;
color: #333333;
line-height: 22px;
margin-bottom: 8px;
}
&__content {
font-size: 14px;
font-weight: 400;
color: #666666;
.option-box {
color: #666666;
margin-bottom: 8px;
.option-box-header {
margin-bottom: 8px;
.option-sort {
display: inline-block;
margin-right: 5px;
}
.input-box {
display: inline-block;
max-width: calc(100% - 20px);
vertical-align: top;
}
}
.picture-box {
width: 88px;
height: 88px;
border-radius: 4px;
overflow: hidden;
align-items: center;
justify-content: center;
margin-right: 12px;
position: relative;
display: inline-flex;
border: 1px solid #e8e8e8;
img {
max-width: 100%;
max-height: 100%;
border-radius: 4px;
vertical-align: middle;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.voice-box {
margin-bottom: 12px;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
width: 320px;
}
}
}
}
.question-answer {
margin-bottom: 16px;
border-bottom: 1px solid #E8E8E8;
padding-bottom: 16px;
img {
max-width: 88px;
max-height: 88px;
}
&__title {
height: 22px;
font-size: 16px;
color: #333333;
line-height: 22px;
margin-bottom: 8px;
}
&__content {
font-size: 14px;
font-weight: 400;
color: #666666;
.option-sort {
display: inline-block;
margin-right: 8px;
}
}
.question-gap-answer {
margin-bottom: 8px;
.gap-label {
display: inline-block;
width: 48px;
height: 20px;
color: #666666;
line-height: 20px;
}
.gap-content {
display: inline-block;
span {
background: #f7f8f9;
border-radius: 2px;
padding: 2px 12px;
margin-right: 8px;
}
}
}
}
.question-desc {
margin-bottom: 16px;
&__title {
height: 22px;
font-size: 16px;
color: #333333;
line-height: 22px;
margin-bottom: 8px;
}
&__content {
font-size: 14px;
font-weight: 400;
color: #666666;
.desc-input-box {
margin-bottom: 8px;
* {
display: inline-block;
}
}
.desc-picture-box {
display: inline-flex;
margin-bottom: 16px;
.picture-box {
width: 88px;
height: 88px;
border-radius: 4px;
overflow: hidden;
align-items: center;
justify-content: center;
margin-right: 12px;
position: relative;
display: inline-flex;
border: 1px solid #e8e8e8;
.img-box {
max-width: 100%;
max-height: 100%;
border-radius: 4px;
vertical-align: middle;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.desc-audio-box {
margin-bottom: 16px;
.audio-box {
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.1);
padding: 10px 20px;
width: 320px;
margin-bottom: 12px;
}
}
.desc-video-box {
.video-box {
position: relative;
display: inline-block;
width: 208px;
height: calc(208px * 9 / 16);
position: relative;
background-color: #000;
margin: 0px 12px 12px 0;
&_content {
max-width: 100%;
max-height: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&_btn {
width: 32px;
height: 32px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -16px;
margin-left: -16px;
}
.icon_arrow {
position: absolute;
top: -12px;
right: -8px;
color: #bfbfbf;
cursor: pointer;
font-size: 16px;
}
}
}
}
}
}
.question-preview-modal.ant-modal {
max-height: 60% !important;
}
...@@ -2,118 +2,146 @@ ...@@ -2,118 +2,146 @@
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-04-29 10:26:32 * @Date: 2020-04-29 10:26:32
* @LastEditors: zangsuyun * @LastEditors: zangsuyun
* @LastEditTime: 2021-03-22 13:55:36 * @LastEditTime: 2021-03-27 10:04:47
* @Description: 内容线路由配置 * @Description: 内容线路由配置
*/ */
import Home from '@/modules/home/Home'; import Home from "@/modules/home/Home";
import EmployeesManagePage from '@/modules/store-manage/EmployeesManagePage'; import EmployeesManagePage from "@/modules/store-manage/EmployeesManagePage";
import personalInfoPage from '@/modules/personalInfo'; import personalInfoPage from "@/modules/personalInfo";
import UserManagePage from '@/modules/store-manage/UserManagePage'; import UserManagePage from "@/modules/store-manage/UserManagePage";
import StoreDecorationPage from '@/modules/store-manage/StoreDecorationPage'; import StoreDecorationPage from "@/modules/store-manage/StoreDecorationPage";
import CourseCatalogPage from '@/modules/store-manage/CourseCatalogPage'; import CourseCatalogPage from "@/modules/store-manage/CourseCatalogPage";
import LiveCoursePage from '@/modules/course-manage/LiveCoursePage'; import LiveCoursePage from "@/modules/course-manage/LiveCoursePage";
import AddLivePage from '@/modules/course-manage/AddLive' import AddLivePage from "@/modules/course-manage/AddLive";
import VideoCoursePage from '@/modules/course-manage/video-course' import VideoCoursePage from "@/modules/course-manage/video-course";
import AddVideoCoursePage from '@/modules/course-manage/video-course/AddVideoCourse' import GraphicsCoursePage from "@/modules/course-manage/graphics-course";
//TODO import AddVideoCoursePage from "@/modules/course-manage/video-course/AddVideoCourse";
import AddGraphicsCoursePage from "@/modules/course-manage/graphics-course/AddGraphicsCourse";
// import DataList from '@/modules/course-manage/DataList/DataList'; // import DataList from '@/modules/course-manage/DataList/DataList';
// import ClassBook from '@/modules/resource-disk'; // import ClassBook from '@/modules/resource-disk';
import ResourceDisk from '@/modules/resource-disk'; import ResourceDisk from "@/modules/resource-disk";
import SwitchRoute from '@/modules/root/SwitchRoute'; import SwitchRoute from "@/modules/root/SwitchRoute";
import PlanPage from '@/modules/plan-manage/PlanPage'; import PlanPage from "@/modules/plan-manage/PlanPage";
import AddPlanPage from '@/modules/plan-manage/AddPlan'; import AddPlanPage from "@/modules/plan-manage/AddPlan";
import LearningDataPage from '@/modules/plan-manage/LearningData'; import LearningDataPage from "@/modules/plan-manage/LearningData";
import StoreInfoPage from '@/modules/store-manage/StoreInfo'; import StoreInfoPage from "@/modules/store-manage/StoreInfo";
import KnowledgeBase from '@/modules/knowledge-base'; import KnowledgeBase from "@/modules/knowledge-base";
import QuestionBankIndex from "@/modules/teach-tool/QuestionBankIndex";
import QuestionCategoryManage from "@/modules/teach-tool/QuestionCategoryManage";
import AddNewQuestion from "@/modules/teach-tool/AddNewQuestion";
const mainRoutes = [ const mainRoutes = [
{ {
path: '/home', path: "/home",
component: Home, component: Home,
name: '中心首页' name: "中心首页",
}, },
{ {
path: '/employees-manage', path: "/employees-manage",
component: EmployeesManagePage, component: EmployeesManagePage,
name: '员工管理' name: "员工管理",
}, },
{ {
path: '/personal-info', path: "/personal-info",
component: personalInfoPage, component: personalInfoPage,
name: '个人信息' name: "个人信息",
}, },
{ {
path: '/user-manage', path: "/user-manage",
component: UserManagePage, component: UserManagePage,
name: '用户管理' name: "用户管理",
}, },
{ {
path: '/store-decoration', path: "/store-decoration",
component: StoreDecorationPage, component: StoreDecorationPage,
name: '店铺装修' name: "店铺装修",
}, },
{ {
path: '/course-catalog', path: "/course-catalog",
component:CourseCatalogPage, component: CourseCatalogPage,
name: '课程分类' name: "课程分类",
}, },
{ {
path: '/live-course', path: "/live-course",
component:LiveCoursePage, component: LiveCoursePage,
name: '直播课' name: "直播课",
}, },
{ {
path: '/video-course', path: "/video-course",
component:VideoCoursePage, component: VideoCoursePage,
name: '视频课' name: "视频课",
}, },
{ {
path: '/create-live-course', path: "/graphics-course",
component:AddLivePage, component: GraphicsCoursePage,
name: '创建直播课' name: "图文课",
}, },
{ {
path: '/create-video-course', path: "/create-live-course",
component:AddVideoCoursePage, component: AddLivePage,
name: '创建视频课' name: "创建直播课",
}, },
{ {
path: '/knowledge-base', path: "/create-video-course",
component: AddVideoCoursePage,
name: "创建视频课",
},
{
path: "/knowledge-base",
// component:ResourceDisk, // component:ResourceDisk,
component:KnowledgeBase, component: KnowledgeBase,
name: '知识库' name: "知识库",
},
{
path: "/create-graphics-course",
component: AddGraphicsCoursePage,
name: "创建图文课",
},
{
path: "/resource-disk",
component: ResourceDisk,
name: "资料云盘",
}, },
{ {
path: '/resource-disk', path: "/question-bank-index",
component:ResourceDisk, component: QuestionBankIndex,
name: '资料云盘' name: "题库",
}, },
{ {
path: '/switch-route', path: "/question-category-manage",
component: QuestionCategoryManage,
name: "分类管理",
},
{
path: "/create-new-question",
component: AddNewQuestion,
name: "新增题目",
},
{
path: "/switch-route",
component: SwitchRoute, component: SwitchRoute,
name: '登录后跳转承载页' name: "登录后跳转承载页",
}, },
{ {
path:'/plan', path: "/plan",
component: PlanPage, component: PlanPage,
name: '培训计划' name: "培训计划",
}, },
{ {
path: '/create-plan', path: "/create-plan",
component:AddPlanPage, component: AddPlanPage,
name: '创建视频课' name: "创建视频课",
}, },
{ {
path: '/store-info', path: "/store-info",
component:StoreInfoPage, component: StoreInfoPage,
name: '店铺信息' name: "店铺信息",
}, },
{ {
path: '/learning-data', path: "/learning-data",
component:LearningDataPage, component: LearningDataPage,
name: '学习数据' name: "学习数据",
} },
];
]
export default mainRoutes; export default mainRoutes;
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* @Author: zangsuyun * @Author: zangsuyun
* @Date: 2021-03-04 12:56:04 * @Date: 2021-03-04 12:56:04
* @LastEditors: zangsuyun * @LastEditors: zangsuyun
* @LastEditTime: 2021-03-25 12:31:18 * @LastEditTime: 2021-03-27 10:06:10
* @Copyright: © 2020 杭州杰竞科技有限公司 版权所有 * @Copyright: © 2020 杭州杰竞科技有限公司 版权所有
*/ */
export const menuList: any = [ export const menuList: any = [
...@@ -27,7 +27,12 @@ export const menuList: any = [ ...@@ -27,7 +27,12 @@ export const menuList: any = [
groupName: "视频课", groupName: "视频课",
groupCode: "CourseVideoClass", groupCode: "CourseVideoClass",
link: '/video-course' link: '/video-course'
} },
{
groupName: "图文课",
groupCode: "GraphicLesson",
link: '/graphics-course'
},
] ]
}, },
{ {
...@@ -52,6 +57,18 @@ export const menuList: any = [ ...@@ -52,6 +57,18 @@ export const menuList: any = [
groupCode: "TrainPlan", groupCode: "TrainPlan",
link: '/plan' link: '/plan'
} }
],
},
{
groupName: "助学工具",
groupCode: "AidTool",
icon: '&#xe828;',
children: [
{
groupName: "题库",
groupCode: "QuestionBank",
link: '/question-bank-index'
}
] ]
}, },
{ {
...@@ -74,10 +91,15 @@ export const menuList: any = [ ...@@ -74,10 +91,15 @@ export const menuList: any = [
groupCode: "ShopUser", groupCode: "ShopUser",
link: '/user-manage' link: '/user-manage'
}, },
// {
// groupName: "课程分类",
// groupCode: "CourseCategory",
// link: '/course-catalog'
// },
{ {
groupName: "课程分类", groupName: "课程分类",
groupCode: "CourseCategory", groupCode: "CourseCategory",
link: '/course-catalog' link: '/question-category-manage'
}, },
{ {
groupName: "店铺装修", groupName: "店铺装修",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment