Commit fea42dc1 by yuananting

feat:题库工具初始化

parent 30fd2e03
/* /*
* @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-02-28 14:22:13
* @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>';
......
@font-face { @font-face {
font-family: 'iconfont'; /* project id 2223403 */ font-family: 'iconfont'; /* project id 2223403 */
src: url('//at.alicdn.com/t/font_2223403_qb6r10go4s.eot'); src: url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.eot');
src: url('//at.alicdn.com/t/font_2223403_qb6r10go4s.eot?#iefix') format('embedded-opentype'), src: url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_2223403_qb6r10go4s.woff2') format('woff2'), url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.woff2') format('woff2'),
url('//at.alicdn.com/t/font_2223403_qb6r10go4s.woff') format('woff'), url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.woff') format('woff'),
url('//at.alicdn.com/t/font_2223403_qb6r10go4s.ttf') format('truetype'), url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.ttf') format('truetype'),
url('//at.alicdn.com/t/font_2223403_qb6r10go4s.svg#iconfont') format('svg'); url('//at.alicdn.com/t/font_2223403_uxtdisq90ka.svg#iconfont') format('svg');
} }
.iconfont{ .iconfont{
font-family:"iconfont" !important; font-family:"iconfont" !important;
......
/*
* @Author: yuananting
* @Date: 2021-03-03 15:13:12
* @LastEditors: yuananting
* @LastEditTime: 2021-03-03 15:18:30
* @Description: 助学工具接口
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import Service from "@/common/js/service";
export function queryCategoryTree(params: object) {
return Service.Hades("/private/lesson/queryCategoryTree", params);
}
/* /*
* @Author: 陈剑宇 * @Author: 陈剑宇
* @Date: 2020-05-07 14:43:01 * @Date: 2020-05-07 14:43:01
* @LastEditTime: 2021-01-27 19:52:58 * @LastEditTime: 2021-02-21 17:24:08
* @LastEditors: zhangleyuan * @LastEditors: yuananting
* @Description: * @Description:
* @FilePath: /wheat-web-demo/src/domains/basic-domain/constants.ts * @FilePath: /wheat-web-demo/src/domains/basic-domain/constants.ts
*/ */
import { MapInterface } from '@/domains/basic-domain/interface' import { MapInterface } from '@/domains/basic-domain/interface'
// 默认是 dev 环境 // 默认是 dev 环境
const ENV: string = process.env.DEPLOY_ENV || 'prod'; 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: 吴文洁 * @Author: 吴文洁
* @Date: 2020-08-05 10:12:45 * @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan * @LastEditors: yuananting
* @LastEditTime: 2021-01-27 19:53:30 * @LastEditTime: 2021-02-22 18:02:31
* @Description: 视频课-列表模块 * @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有 * @Copyright: 杭州杰竞科技有限公司 版权所有
*/ */
......
/*
* @Author: yuananting
* @Date: 2021-02-25 13:46:35
* @LastEditors: yuananting
* @LastEditTime: 2021-03-09 11:19:01
* @Description: 助学工具-题库-题目管理-新增题目
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Tabs, Button, Tooltip } from "antd";
import Breadcrumbs from "@/components/Breadcrumbs";
import ShowTips from "@/components/ShowTips";
import "./AddNewQuestion.less";
import NewQuestionTab from "./components/NewQuestionTab";
import { defineQuestionData } from "./components/model";
const { TabPane } = Tabs;
class AddNewQuestion extends Component {
constructor(props) {
super(props);
this.state = {
activeKey: "THE_RADIO",
singleContent: defineQuestionData("THE_RADIO"), // 单选题内容
multipleContent: defineQuestionData("MULTI_SELECT"), // 多选题内容
judgeContent: defineQuestionData("JUDGE"), // 多选题内容
completionContent: defineQuestionData("COMPLETION"), // 填空题内容
indefiniteContent: defineQuestionData("INDEFINITE_SELECT"), // 不定项选择题内容
};
}
componentDidMount() {}
saveCurrentQuestion = () => {
switch (this.state.activeKey) {
case "THE_RADIO":
console.log(this.singleRef)
this.singleRef.checkInput();
break;
case "MULTI_SELECT":
this.multipleRef.checkInput();
break;
case "JUDGE":
this.judgeRef.checkInput();
break;
case "COMPLETION":
this.CompletionRef.checkInput();
break;
case "INDEFINITE_SELECT":
this.indefiniteRef.checkInput();
break;
}
};
render() {
const {
activeKey,
singleContent,
multipleContent,
judgeContent,
completionContent,
indefiniteContent,
} = this.state;
return (
<div className="page add-new-question">
<Breadcrumbs navList="新增题目" goBack={() => RCHistory.goBack()} />
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企培保有依据国家规定及平台规则进行处理的权利" />
</div>
<Tabs
style={{ marginTop: 32 }}
activeKey={activeKey}
onChange={(activeKey) => {
this.setState({ activeKey });
}}
>
<TabPane
tab={<span className="icon iconfont">&#xe7fa; 单选题</span>}
key="THE_RADIO"
>
<NewQuestionTab
questionTypeKey="THE_RADIO"
onRef={(ref) => {
this.singleRef = ref;
}}
questionContent={singleContent}
onSetState={(newContent) => {
Object.assign(singleContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={<span className="icon iconfont">&#xe7fb; 多选题</span>}
key="MULTI_SELECT"
>
<NewQuestionTab
questionTypeKey="MULTI_SELECT"
onRef={(ref) => {
this.multipleRef = ref;
}}
questionContent={multipleContent}
onSetState={(newContent) => {
Object.assign(multipleContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={<span className="icon iconfont">&#xe7fc; 判断题</span>}
key="JUDGE"
>
<NewQuestionTab
questionTypeKey="JUDGE"
onRef={(ref) => {
this.judgeRef = ref;
}}
questionContent={judgeContent}
onSetState={(newContent) => {
Object.assign(judgeContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={<span className="icon iconfont">&#xe7fd; 填空题</span>}
key="COMPLETION"
>
<NewQuestionTab
questionTypeKey="COMPLETION"
onRef={(ref) => {
this.CompletionRef = ref;
}}
questionContent={completionContent}
onSetState={(newContent) => {
Object.assign(completionContent, newContent);
}}
/>
</TabPane>
<TabPane
tab={
<span className="icon iconfont">
&#xe7fe; 不定项选择题{" "}
<Tooltip title="至少有一项正确,至多不限的选择题,多项选择题的一种特殊形式">
<span style={{ color: "#BFBFBF" }}>&#xe7c4;</span>
</Tooltip>
</span>
}
key="INDEFINITE_SELECT"
>
<NewQuestionTab
questionTypeKey="INDEFINITE_SELECT"
onRef={(ref) => {
this.indefiniteRef = ref;
}}
questionContent={indefiniteContent}
onSetState={(newContent) => {
Object.assign(indefiniteContent, newContent);
}}
/>
</TabPane>
</Tabs>
</div>
<div className="footer">
<Button>取消</Button>
<Button>保存并继续添加</Button>
<Button
type="primary"
onClick={() => {
this.saveCurrentQuestion();
}}
>
保存
</Button>
</div>
</div>
);
}
}
export default AddNewQuestion;
/*
* @Author: yuananting
* @Date: 2021-02-25 13:52:01
* @LastEditors: yuananting
* @LastEditTime: 2021-03-08 19:07:56
* @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-05 13:41:32
* @Description: 助学工具-题库-题库主页面
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Layout } from "antd";
import "./QuestionBankIndex.less";
import QuestionBankSider from "./components/QuestionBankSider";
import QuestionManageFilter from "./components/QuestionManageFilter";
import QuestionManageOpt from "./components/QuestionManageOpt";
import QuestionManageList from "./components/QuestionManageList";
class QuestionBankIndex extends Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
},
loading: true,
dataSource: [], // 题库列表数据
totalCount: 1, // 题库数据总条数
};
}
componentDidMount() {
// TODO
// 接口请求 初始化数据
this.getQuestionBankList();
}
getQuestionBankList = (_query = {}) => {
const query = {
...this.state.query,
..._query,
};
// 更新请求参数
this.setState({ query });
// CourseService.videoSchedulePage(query).then((res) => {
// const { result = {} } = res || {};
// const { records = [], total = 0 } = result;
// this.setState({
// dataSource: records,
// totalCount: Number(total)
// });
// });
};
render() {
const { query, dataSource, totalCount } = this.state;
return (
<div className="question-bank-index page">
<div className="content-header">题目</div>
<div className="box content-body">
<div className="sider">
<QuestionBankSider />
</div>
<div className="content">
<QuestionManageFilter />
<QuestionManageOpt />
<QuestionManageList
query={query}
dataSource={dataSource}
totalCount={totalCount}
onChange={this.getQuestionList}
/>
</div>
</div>
</div>
);
}
}
export default QuestionBankIndex;
/*
* @Author: yuananting
* @Date: 2021-02-21 18:27:43
* @LastEditors: yuananting
* @LastEditTime: 2021-03-02 17:26:30
* @Description: 助学工具-题库-题库主页面样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-bank-index {
.content-body {
display: flex;
.site-layout-background {
background: #fff;
}
.sider {
min-width: 260px;
}
.content {
width: 100%;
margin-left: 24px;
}
}
}
/*
* @Author: yuananting
* @Date: 2021-02-23 18:28:50
* @LastEditors: yuananting
* @LastEditTime: 2021-03-05 10:19:34
* @Description: 助学工具-题库-主页面分类管理
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import Breadcrumbs from "@/components/Breadcrumbs";
import "./QuestionCategoryManage.less";
import NewEditQuestionBankCategory from "./modal/NewEditQuestionBankCategory";
import { Tree, Input, Space, Button, Menu, Dropdown, message } from "antd";
import ShowTips from "@/components/ShowTips";
const { DirectoryTree, Popconfirm } = Tree;
const { Search } = Input;
class QuestionCategoryManage extends Component {
constructor(props) {
super(props);
this.state = {
NewEditQuestionBankCategory: null, //新增或编辑分类模态框
treeData: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023655839768500",
parentId: "0",
rootId: "1367023655839768500",
categoryLevel: 0,
categoryName: "未分类",
sort: 0,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023655839768577",
parentId: "0",
rootId: "1367023655839768577",
categoryLevel: 0,
categoryName: "分类1",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023709942095873",
parentId: "1367023655839768577",
rootId: "1367023655839768577",
categoryLevel: 1,
categoryName: "test",
sort: 0,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023725184196609",
parentId: "1367023655839768577",
rootId: "1367023655839768577",
categoryLevel: 1,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023805563838466",
parentId: "1367023725184196609",
rootId: "1367023655839768577",
categoryLevel: 2,
categoryName: "test",
sort: 0,
},
],
sort: 1,
},
],
sort: 1,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049864988516353",
parentId: "0",
rootId: "1367049864988516353",
categoryLevel: 0,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049931656978433",
parentId: "1367049864988516353",
rootId: "1367049864988516353",
categoryLevel: 1,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049973746819074",
parentId: "1367049931656978433",
rootId: "1367049864988516353",
categoryLevel: 2,
categoryName: "分类3",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367050021431861249",
parentId: "1367049973746819074",
rootId: "1367049864988516353",
categoryLevel: 3,
categoryName: "分类4",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367050063035162626",
parentId: "1367050021431861249",
rootId: "1367049864988516353",
categoryLevel: 4,
categoryName: "分类5",
sort: 0,
},
],
sort: 0,
},
],
sort: 0,
},
],
sort: 0,
},
],
sort: 2,
},
],
treeMap: {},
};
}
componentDidMount() {
this.setState({ treeData: this.renderTreeNodes(this.state.treeData) });
this.setState({
expandedKeys: this.getFirstLevelKeys(this.state.treeData),
});
}
// 新增分类
newEditQuestionCategory = (categoryType, id, parentId, addLevelType) => {
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
id={id}
parentId={parentId}
addLevelType={addLevelType}
treeData={this.state.treeData}
title={title}
label={label}
close={() => {
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,
item.id,
item.parentId,
"equal"
);
}}
>
重命名
</span>
</Menu.Item>
<Menu.Item key="1">
<span>删除</span>
</Menu.Item>
</Menu>
);
};
getRelatedNodes = (parentId) => {
console.log("parentId:", parentId);
return this.state.treeMap[parentId].subList
? this.state.treeMap[parentId].subList
: [];
};
onDrop = (info) => {
console.log("info:", info);
// 未分类不可以拖拽
if (info.dragNode.id === "1367023655839768500") return;
// 不允许其他节点拖拽到未分类之前
if (
info.node.id === "1367023655839768500" &&
info.dropToGap &&
info.dropPosition === -1
)
return;
let targetParentId = info.dropToGap ? info.node.parentId : info.node.id;
let relatedNodes = this.getRelatedNodes(targetParentId);
console.log(
"relatedNodes:",
relatedNodes.length,
relatedNodes,
targetParentId
);
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].subList) {
loop(data[i].subList, key, callback);
}
}
};
const data = [...this.state.treeData];
let getSuf = function (name, sufIndex) {
if (relatedNodes.length > 0) {
let sameNameNodes = [];
relatedNodes.forEach((item) => {
if (item.id === info.dragNode.id) return true;
if (item.categoryName === name) {
sameNameNodes.push(item);
console.log(item, sameNameNodes);
}
});
if (sameNameNodes.length > 0) {
sufIndex++;
return getSuf(name + `(${sufIndex})`, 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;
console.log("info.dragNode.categoryName:", info.dragNode.categoryName);
let sufIndex = getSuf(info.dragNode.categoryName, 0);
console.log("sufIndex1:", sufIndex);
console.log("sufIndex2:", sufIndex > 0 ? `${sufIndex}` : "");
item.categoryName =
item.originCategoryName + (sufIndex ? `(${sufIndex})` : "");
dragObj = item;
});
if (!info.dropToGap) {
loop(data, dropKey, (item) => {
item.subList = item.subList || [];
item.subList.push(dragObj);
});
} else if (
(info.node.props.subList || []).length > 0 &&
info.node.props.expanded &&
dropPosition === 1
) {
loop(data, dropKey, (item) => {
item.subList = item.children || [];
item.subList.push(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);
}
}
console.log("data:", data)
this.setState({ treeData: this.renderTreeNodes(this.handleLoop(data, 0)) });
};
handleLoop = (data, level) => {
data.map((item, index) => {
item.categoryLevel = level;
item.sort = index;
if (item.children) {
this.handleLoop(item.children, 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];
mouseNodeOpts.style.visibility = "visible";
}}
onMouseOut={(e) => {
let mouseNodeOpts = e.currentTarget.getElementsByTagName("div")[0];
mouseNodeOpts.style.visibility = "hidden";
}}
>
<span>{item.categoryName}</span>
{item.id !== "1367023655839768500" && (
<Space className="title-opts" size={50}>
<span
className="icon iconfont"
onClick={() => {
let nodesCount = 0;
const { treeData } = this.state;
if (item.categoryLevel === 0) {
// 第一层级
nodesCount = treeData.length;
} else {
let parentNodes = this.getRelatedNodes(item.parentId);
nodesCount =
parentNodes.length > 0
? parentNodes[0].subList.length
: 0;
}
if (nodesCount >= 30) {
message.info("最多只能添加30个分类");
return;
}
this.newEditQuestionCategory(
"newEqualLevelCategory",
item.id,
item.parentId,
"equal"
);
}}
>
&#xe7f5; 同级
</span>
{item.categoryLevel < 4 && (
<span
className="icon iconfont"
onClick={() => {
if (item.subList && item.subList.length >= 30) {
message.info("最多只能添加30个子分类");
return;
}
this.newEditQuestionCategory(
"newChildLevelCategory",
item.id,
item.parentId,
"child"
);
}}
>
&#xe7f8; 子级
</span>
)}
<Dropdown overlay={this.initDropMenu(item)}>
<span className="icon iconfont">更多 &#xe7f7;</span>
</Dropdown>
</Space>
)}
</div>
);
item.icon =
item.id === "default" ? (
<span className="icon iconfont" style={{ color: "#FBD140" }}>
&#xe7f6;
</span>
) : (
<span className="icon iconfont" style={{ color: "#FBD140" }}>
&#xe7f1;
</span>
);
if (item.subList) {
item.children = this.renderTreeNodes(item.subList, value);
}
return item;
});
this.getFirstLevelKeys(newTreeData);
let map = {};
let getChildren = function (data) {
data.forEach((item) => {
map[item.id] = item;
if (item.subList && item.subList.length > 0) {
getChildren(item.subList);
}
});
};
getChildren(data);
console.log("map:", map);
this.setState({ treeMap: map });
return newTreeData;
};
render() {
const { treeData, expandedKeys } = this.state;
return (
<div className="page question-category-manage">
<Breadcrumbs navList="课程分类" goBack={() => RCHistory.goBack()} />
<div className="box">
<div className="search-condition">
<span className="search-label">搜索名称:</span>
<Search
placeholder="请输入名称"
style={{ width: "calc(100% - 84px)" }}
onSearch={(value) =>
// TODO 调用查询分类接口
this.setState({
treeData: this.renderTreeNodes(treeData, value),
})
}
/>
</div>
<Button
type="primary"
onClick={() => {
if (treeData.length >= 30) {
message.info("最多只能添加30个分类");
return;
}
this.newEditQuestionCategory(
"newEqualLevelCategory",
"1367023655839768500",
"0",
"equal"
);
}}
>
新增一级分类
</Button>
<div className="show-tips" style={{ marginTop: 12 }}>
<ShowTips message="为方便管理,该分类用于课程、培训计划、题库、知识库等模块,改动将同步各模块更新" />
</div>
<div className="course-category-tree">
<DirectoryTree
expandedKeys={expandedKeys}
onExpand={this.onExpand}
defaultSelectedKeys={["1367023655839768500"]}
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-05 09:34:00
* @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;
.ant-tree.ant-tree-directory {
font-size: 14px;
font-weight: 400;
color: #666666;
.anticon {
color: #666666;
}
.ant-tree-treenode {
height: 44px;
padding: 0;
span {
line-height: 44px;
}
.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;
}
}
}
}
}
// .ant-tree-treenode-selected:hover::before,
// .ant-tree-treenode-selected::before {
// background: rgb(255 251 240);
// }
}
}
.xm-show-tip {
background: #f1f3f6 !important;
span.icon {
color: #bfbfbf !important;
}
}
}
/*
* @Author: yuananting
* @Date: 2021-02-25 09:45:11
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 11:35:10
* @Description: 助学工具-题库-题目管理主页面
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import Breadcrumbs from "@/components/Breadcrumbs";
import { Space } from "antd";
import QuestionManageFilter from "./components/QuestionManageFilter";
import QuestionManageOpt from "./components/QuestionManageOpt";
import QuestionManageList from "./components/QuestionManageList";
import "./QuestionManageIndex.less";
class QuestionManageIndex extends Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
},
loading: true,
dataSource: [], // 题目列表数据
totalCount: 1, // 题目数据总条数
};
}
componentDidMount() {
// TODO
// 接口请求 初始化数据
this.getQuestionList();
}
getQuestionList = (_query = {}) => {
const query = {
...this.state.query,
..._query,
};
// 更新请求参数
this.setState({ query });
};
render() {
const { query, dataSource, totalCount } = this.state;
return (
<div className="question-manage-index page">
<Breadcrumbs navList="课程分类" goBack={() => RCHistory.goBack()} />
<div className="box">
<Space className="question-basic-info" size={100}>
<span>题库名称:{"业务培训"}</span>
<span>题目数量:{100}</span>
</Space>
<QuestionManageFilter />
<QuestionManageOpt />
<QuestionManageList
query={query}
dataSource={dataSource}
totalCount={totalCount}
onChange={this.getQuestionList}
/>
</div>
</div>
);
}
}
export default QuestionManageIndex;
/*
* @Author: yuananting
* @Date: 2021-02-25 10:49:16
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 10:53:58
* @Description: 助学工具-题库-题目管理主页面样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-manage-index {
position: relative;
.question-basic-info {
margin-bottom: 16px;
}
}
import React, { Component } from "react";
import { message, Button } from "antd";
import UploadOss from "@/core/upload";
import "./CompletionStem.less";
const MEDIA_MAP = [
{
title: "音频",
icon: <React.Fragment>&#xe756;</React.Fragment>,
key: "VOICE",
},
{
title: "录音",
icon: <React.Fragment>&#xe7bb;</React.Fragment>,
key: "RECORD",
},
{
title: "图片",
icon: <React.Fragment>&#xe758;</React.Fragment>,
key: "PICTURE",
},
{
title: "视频",
icon: <React.Fragment>&#xe755;</React.Fragment>,
key: "VIDEO",
},
];
class CompletionStem extends Component {
constructor(props) {
super(props);
this.state = {
visibleMediaBox: false,
zIndex: 9,
focusFlag: false,
isShowSingleInput: true,
contentLength: 0,
errorInput: false,
blanksList: [],
originBlanksList: [],
};
}
componentDidUpdate() {
const stemInput = document.getElementById("editor-box_content");
stemInput.addEventListener("DOMSubtreeModified", this.watchStemContent);
}
watchStemContent = () => {
var currentBlanksList = this.saveCurrentBlanks(); // 获取当前填空列表
console.log("originBlanksList:", this.state.originBlanksList);
console.log("currentBlanksList:", currentBlanksList);
// console.log(JSON.stringify(currentBlanksList)===JSON.stringify(this.state.originBlanksList))
if (
JSON.stringify(currentBlanksList) !==
JSON.stringify(this.state.originBlanksList)
) {
console.log("修改")
this.setState({ originBlanksList: this.saveCurrentBlanks() }); // 保存修改前的填空列表
this.sortBlanks();
}
this.handleStemStyle();
this.props.changeBlankCount(currentBlanksList);
};
sortBlanks = () => {
const _blanksList = JSON.parse(JSON.stringify(this.saveCurrentBlanks()));
_blanksList.map((item, index) => {
item.innerHTML = "填空" + (index + 1);
return item;
});
this.setState({ blanksList: _blanksList });
};
saveCurrentBlanks = () => {
var currentBlanksList = [];
var stemInput = document.getElementById("editor-box_content");
stemInput.childNodes.forEach((item) => {
if (item.nodeName === "SPAN") {
currentBlanksList.push(item);
}
});
return currentBlanksList;
};
handleInsertBlanks = (id) => {
document.getElementById("editor-box_content").focus();
const { blanksList } = this.state;
blanksList.map((item, index) => {
item.innerHTML = "填空" + (index + 1);
});
var templateBlanks = blanksList.filter((item) => item.id === id);
var insertBlanks = document.createElement("span");
insertBlanks.className = "fill-line";
insertBlanks.innerHTML = templateBlanks[0].innerHTML;
insertBlanks.id = templateBlanks[0].id; // 填空id;
insertBlanks.contentEditable = false;
var sel, range;
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
var el = document.createElement("div");
el.appendChild(insertBlanks);
var frag = document.createDocumentFragment(),
node,
lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
};
/**
* 插入占位符
*
* @memberof QuestionInputItem
*/
insertBlanks = () => {
const _blanksList = this.state.blanksList;
var blanks = document.createElement("span");
blanks.className = "fill-line";
blanks.innerHTML = "填空";
blanks.id = window.random_string(16); // 填空id
blanks.contentEditable = false;
_blanksList.push(blanks);
this.setState({ blanksList: _blanksList });
this.handleInsertBlanks(blanks.id);
};
// 输入框样式
handleStemStyle = () => {
const stemInput = document.getElementById("editor-box_content");
const textLength = stemInput.innerText.replace(/\&nbsp\;/gi, " ").length;
const imgLength = stemInput.innerHTML.match(/<img/g)
? stemInput.innerHTML.match(/<img/g).length * 2
: 0;
const contentLength = imgLength + textLength;
this.setState({ contentLength });
const divHeight = document.getElementById("editor-box_content")
.offsetHeight;
if (divHeight > 22 || imgLength > 0) {
this.setState({ isShowSingleInput: false });
} else {
this.setState({ isShowSingleInput: true });
}
};
handleUploadMedia = (key) => {
this.props.onUploadMedia && this.props.onUploadMedia(key);
};
render() {
const {
visibleMediaBox,
zIndex,
focusFlag,
contentLength,
isShowSingleInput,
errorInput,
} = this.state;
const {
mediaBtn = ["VOICE", "RECORD", "PICTURE", "VIDEO"],
limitLength = 1000,
} = this.props;
return (
<div
className="completion-question-edtior_box"
style={{ zIndex }}
onMouseEnter={() => {
if (visibleMediaBox || focusFlag) return;
const setZIndex = 101;
this.setState({
visibleMediaBox: true,
zIndex: setZIndex,
});
}}
onMouseLeave={() => {
if (!visibleMediaBox || focusFlag) return;
this.setState({
visibleMediaBox: false,
zIndex: 9,
});
}}
>
<div
className={
isShowSingleInput ? "editor-box-single " : "editor-box-multiple"
}
style={{
border:
this.props.validateStatus === "error" ? "1px solid red" : "",
}}
>
<div
placeholder="示例:党章规定,凡事有&nbsp;&nbsp;填空1&nbsp;&nbsp;
人以上的&nbsp;&nbsp;填空2&nbsp;&nbsp;
,都应该成立党的基层组织"
contentEditable
suppressContentEditableWarning
className="editor-box_content"
id="editor-box_content"
></div>
<div className="editor-limit">
<span style={{ color: errorInput ? "red" : "" }}>
{contentLength}
</span>
/{limitLength}
</div>
</div>
<div className="editor-fill-info">
在需要填写答案的地方
<Button
type="link"
className="editor-fill-info_icon icon iconfont"
onClick={this.insertBlanks}
>
&#xe7fd; 插入占位符
</Button>
</div>
<div
className={`editor-limit-tip${
contentLength > limitLength ? " mt6" : ""
}`}
style={{ height: contentLength > limitLength ? 20 : 0 }}
>
最多只能输入1000字
</div>
{visibleMediaBox && !_.isEmpty(mediaBtn) && (
<div className="edtior-media_box">
<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 CompletionStem;
.completion-question-edtior_box {
position: relative;
.editor-box-single {
border-radius: 4px;
padding: 4px 0 4px 10px;
border: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
.editor-box_content {
width: calc(100% - 80px);
.fill-line {
padding: 0 10px;
border-bottom: 1px solid;
}
}
.editor-limit {
padding-right: 12px;
line-height: 22px;
font-size: 12px;
color: #cccccc;
}
}
.editor-box-multiple {
border-radius: 4px;
padding: 4px 0 4px 10px;
border: 1px solid #e8e8e8;
.editor-box_content {
display: inline-block;
overflow-y: scroll;
max-height: 110px;
width: 100%;
.fill-line {
padding: 0 10px;
border-bottom: 1px solid;
}
}
.editor-limit {
text-align: right;
padding-right: 12px;
line-height: 22px;
font-size: 12px;
color: #cccccc;
padding-top: 3px;
}
}
.editor-box_content:empty:before {
content: attr(placeholder);
color: #ddd;
}
.editor-box_content:focus {
border: none;
outline: none;
}
.editor-fill-info {
height: 20px;
font-size: 14px;
line-height: 20px;
color: #999999;
margin-top: 8px;
.editor-fill-info_icon {
color: #5289fa;
font-size: 14px;
padding-left: 9px;
cursor: pointer;
}
}
.editor-limit-tip {
height: 20px;
text-align: right;
color: #ec4b35;
transition: height 0.5s ease-in-out;
overflow: hidden;
}
.editor-placehold {
position: absolute;
top: 0;
left: 0;
right: 0;
font-size: 14px;
color: #cccccc;
line-height: 22px;
margin: 6px 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.edtior-media_box {
position: absolute;
top: 100%;
left: 0;
padding-top: 9px;
&::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 14:34:29
* @LastEditors: yuananting
* @LastEditTime: 2021-03-11 09:37:11
* @Description: 助学工具-题库-题目管理-新建题目Tab
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Form, Radio, message, Checkbox, Tag, Tooltip, Input } from "antd";
import "./NewQuestionTab.less";
import Upload from "@/core/upload";
import QuestionEditor from "./QuestionEditor";
import CompletionStem from "./CompletionStem";
import { PlusOutlined, CloseOutlined } from "@ant-design/icons";
import {
NUM_TO_WORD_MAP,
QUESTION_FILE_ACCEPT,
} from "@/common/constants/punchClock/punchClock";
import { defineOptionData } from "./model";
import UploadingProgress from "./UploadingProgress";
import XMAudio from "./XMAudio";
import XMRecord from "./XMRecord";
import ScanFileModal from "@/modules/resource-disk/modal/ScanFileModal";
class NewQuestionTab extends Component {
constructor(props) {
super(props);
const { questionContent = {} } = props;
const {
topicDescribeVOList,
itemInfoVOList,
parsingDescribeVOList,
taskModuleQuestionVO,
showBox,
} = questionContent;
this.state = {
questionDetail: JSON.parse(JSON.stringify(taskModuleQuestionVO)), // 题目其他信息
stemContent: JSON.parse(JSON.stringify(topicDescribeVOList)), // 题干内容
chooseOptions: JSON.parse(JSON.stringify(itemInfoVOList)), // 单选多选不定项-选项列表
analysisContent: JSON.parse(JSON.stringify(parsingDescribeVOList)), // 答案解析
accept: QUESTION_FILE_ACCEPT["PICTURE"], // 上传媒体类型
fileType: "PICTURE", // 媒体枚举
showRecord: false, // 录音弹窗
showBox: showBox,
completionOptions: [],
blanksList: [], // 填空列表
judgeOptions: [
{
// 判断-选项列表
label: "正确",
value: true,
ifCorrectAnswerItem: null,
},
{
label: "错误",
value: false,
ifCorrectAnswerItem: null,
},
],
};
this.uploadInput = React.createRef();
this.markKey = window.random_string(16);
}
componentDidMount() {
const { chooseOptions } = this.state;
if (chooseOptions.length === 0) {
// 选择题(单选 多选 不定项)-插入4条默认选项
for (var i = 0; i < 4; i++) {
this.handleAddOption();
this.setState({ [`optionsValidate_${i}`]: "success" });
this.setState({ [`optionsText_${i}`]: "" });
}
}
this.props.onRef(this);
}
checkInput = () => {
// 题干校验
const stemContent = _.find(
this.state.stemContent,
(contentItem) => contentItem.contentType === "TEXT"
);
const stem = stemContent.content.replace(/<[^>]+>/g, "");
if (stem.length === 0) {
this.setState({ stemValidate: "error" });
this.setState({ stemText: "请输入题干" });
} else {
this.setState({ stemValidate: "success" });
this.setState({ stemText: "" });
}
// 选项校验
let optionUnChecked = 0;
const { chooseOptions, judgeOptions } = this.state;
const { questionTypeKey } = this.props;
if (questionTypeKey === "JUDGE") {
optionUnChecked = judgeOptions.filter(
(item) => item.ifCorrectAnswerItem === null
).length;
if (optionUnChecked === judgeOptions.length) {
this.setState({ radioValidate: "error" });
this.setState({ radioText: "请选择正确答案" });
} else {
this.setState({ radioValidate: "success" });
this.setState({ radioText: "" });
}
} else {
chooseOptions.forEach((item, index) => {
const optionContent = item.itemContentVOList;
optionUnChecked = item.questionItemVO.ifCorrectAnswerItem
? optionUnChecked
: optionUnChecked + 1;
if (
optionContent.length === 1 &&
optionContent[0].contentType === "TEXT" &&
optionContent[0].content.length === 0
) {
this.setState({ [`optionsValidate_${index}`]: "error" });
this.setState({ [`optionsText_${index}`]: "请输入选项" });
} else {
this.setState({ [`optionsValidate_${index}`]: "success" });
this.setState({ [`optionsText_${index}`]: "" });
}
});
if (optionUnChecked === chooseOptions.length) {
this.setState({ radioValidate: "error" });
this.setState({ radioText: "请选择正确答案" });
} else {
this.setState({ radioValidate: "success" });
this.setState({ radioText: "" });
}
if (
this.props.questionTypeKey === "MULTI_SELECT" &&
this.state.chooseOptions.length - optionUnChecked === 1
) {
this.setState({ radioValidate: "error" });
this.setState({ radioText: "最少选两个" });
}
}
};
static getDerivedStateFromProps(nextProps, prevState) {
// 控制录音组件展示
if (nextProps.showBox && !prevState.showBox) {
return {
showRecord: false,
showBox: nextProps.showBox,
};
}
return {
showBox: nextProps.showBox,
};
}
_onSetState = (params = {}) => {
this.setState({ ...params, updateKey: window.random_string(16) }, () => {
this.props.onSetState({
topicDescribeVOList: JSON.parse(JSON.stringify(this.state.stemContent)),
itemInfoVOList: JSON.parse(JSON.stringify(this.state.chooseOptions)),
parsingDescribeVOList: JSON.parse(
JSON.stringify(this.state.analysisContent)
),
taskModuleQuestionVO: JSON.parse(
JSON.stringify(this.state.questionDetail)
),
});
});
};
/**
* 预览
*
* @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(defineOptionData(content));
this._onSetState();
}
};
/**
* 选择上传文件类型
*
* @memberof QuestionInputItem
*/
handleChangeMedia = (key, uploadItemTarget, inputType) => {
const { mediaFirstType } = this.state;
const mediaArr = _.filter(uploadItemTarget, (mediaItem) => {
return mediaItem.type !== "TEXT";
});
if (mediaArr.length === 0) {
this.setState({ mediaFirstType: key });
}
if (mediaArr.length > 0 && key !== mediaFirstType) {
return message.warning("只能添加1种类型的多媒体文件");
}
switch (inputType) {
case "stem":
if (mediaFirstType === "PICTURE" && mediaArr.length > 8) {
return message.warning("只能添加9张图片");
}
if (mediaFirstType === "VOICE" && mediaArr.length > 2) {
return message.warning("只能添加3个音频");
}
if (mediaFirstType === "RECORD" && mediaArr.length > 2) {
return message.warning("只能添加3个录音");
}
break;
case "options":
if (mediaArr.length > 0) {
return message.warning("只能添加1个多媒体文件");
}
break;
case "analysis":
if (mediaFirstType === "PICTURE" && mediaArr.length > 8) {
return message.warning("只能添加9张图片");
}
if (mediaFirstType === "VOICE" && mediaArr.length > 2) {
return message.warning("只能添加3个音频");
}
if (mediaFirstType === "RECORD" && mediaArr.length > 2) {
return message.warning("只能添加3个录音");
}
if (mediaFirstType === "VIDEO" && mediaArr.length > 2) {
return message.warning("只能添加3个视频");
}
break;
default:
break;
}
this.setState(
{
uploadItemTarget,
},
() => {
QUESTION_FILE_ACCEPT[key] &&
this.setState(
{
accept: QUESTION_FILE_ACCEPT[key],
fileType: key,
},
() => {
this.uploadInput.current.value = "";
this.uploadInput.current.click();
}
);
// 录音
if (key === "RECORD") {
this.setState({
showRecord: true,
});
}
}
);
};
async uploadFile(event) {
const { fileType, uploadItemTarget } = this.state;
const imageFile = event.target.files[0];
if (!imageFile) return;
if (fileType === "VOICE") {
if (!QUESTION_FILE_ACCEPT.VOICE.split(",").includes(imageFile.type)) {
message.warning("文件格式不正确");
return;
}
if (imageFile.size > 30 * 1024 * 1024) {
Modal.warning({
title: "音频过大",
content: "音频大小超过30M,请压缩后上传",
});
return;
}
}
if (fileType === "PICTURE") {
if (!QUESTION_FILE_ACCEPT.PICTURE.split(",").includes(imageFile.type)) {
message.warning("文件格式不正确");
return;
}
if (imageFile.size > 8 * 1024 * 1024) {
Modal.warning({
title: "图片过大",
content: "图片大小超过8M,请压缩后上传",
});
return;
}
}
if (fileType === "VIDEO") {
if (!QUESTION_FILE_ACCEPT.VIDEO.split(",").includes(imageFile.type)) {
message.warning("文件格式不正确");
return;
}
if (imageFile.size > 1024 * 1024 * 1024) {
Modal.warning({
title: "视频过大",
content: "视频大小超过1G,请压缩后上传",
});
return;
}
}
const originArr = imageFile.name.split(".");
const originType = originArr[originArr.length - 1];
const uploadObj = {
contentType: fileType,
type: fileType,
contentName: `${window.random_string(16)}.${originType}`,
status: "init",
key: window.random_string(16),
imageFile,
};
if (["VIDEO", "VOICE"].includes(fileType)) {
try {
await new Promise((resolve) => {
const fileurl = URL.createObjectURL(imageFile);
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);
}
}
uploadItemTarget.push(uploadObj);
this.setState({}, () => {
this.handleReupload(uploadObj);
this.uploadInput.current.value = "";
});
}
changeBlankCount = (data) => {
data.map((item) => {
item.answerTagList = [];
item.inputVisible = false;
item.errorHold = false;
item.editInput = false;
return item;
});
this.setState({ blanksList: data });
};
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) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
if (val) {
optionItem.answerTagList.push(val);
optionItem.inputVisible = false;
} else {
optionItem.errorHold = true;
}
}
});
this.setState({ blanksList: _blanksList });
};
handleInputClose = (optionItem, removedTag) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
optionItem = optionItem.answerTagList.filter(
(tag) => tag !== removedTag
);
}
});
this.setState({ blanksList: _blanksList });
};
handleInputEdit = (optionItem, index) => {
const _blanksList = this.state.blanksList;
_blanksList.forEach((item) => {
if (item.id === optionItem.id) {
item.answerTagList.map()
}
});
this.setState({ blanksList: _blanksList });
}
renderCompletionAnswer = (optionItem) => {
return (
<div className="completion-answer-box">
<span className="completion-answer-label">{optionItem.innerHTML}.</span>
<div className="completion-answer-content">
{optionItem.answerTagList.map((tag, index) => {
return optionItem.editInput ? (
<Input
placeholder={optionItem.errorHold ? "请输入" : ""}
style={{
border: optionItem.errorHold && "1px solid #FF4F4F",
}}
value={tag}
size="small"
suffix={<CloseOutlined style={{ color: "#999999" }} />}
onBlur={(e) =>
this.handleInputConfirm(optionItem, e.target.value, index)
}
onPressEnter={(e) =>
this.handleInputConfirm(optionItem, e.target.value, index)
}
/>
) : (
<Tag
className="edit-tag"
closable
onClose={() => this.handleInputClose(optionItem, tag)}
onDoubleClick={(e) => {
this.handleInputEdit(optionItem, index);
// e.preventDefault();
}}
>
{tag}
</Tag>
);
})}
{optionItem.inputVisible && (
<Input
placeholder={optionItem.errorHold && "请输入"}
style={{
border: optionItem.errorHold && "1px solid #FF4F4F",
}}
size="small"
suffix={<CloseOutlined 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>
);
};
/**
* 渲染输入内容
*
* @memberof QuestionInputItem
*/
renderContent = (
contentList,
placehold,
mediaBtn,
inputType,
validateStatus
) => {
const isCompletion = this.props.questionTypeKey === "COMPLETION";
const textContent = _.find(
contentList,
(contentItem) => contentItem.contentType === "TEXT"
);
return (
<React.Fragment>
<div>
{isCompletion && inputType === "stem" ? (
<CompletionStem
placehold={placehold}
validateStatus={validateStatus}
detail={contentList}
mediaBtn={mediaBtn}
changeBlankCount={this.changeBlankCount.bind(this)}
onUploadMedia={(key) => {
this.handleChangeMedia(key, contentList, inputType);
}}
/>
) : (
<QuestionEditor
markKey={this.markKey}
placehold={placehold}
validateStatus={validateStatus}
detail={contentList}
mediaBtn={mediaBtn}
bindChangeContent={(cb, textElemId) => {
this.setState({ textElemId });
textContent.handleChangeContent = cb;
}}
onChange={(content, textLength) => {
textContent.content = content;
textContent.textLength = textLength;
this._onSetState();
}}
onUploadMedia={(key) => {
this.handleChangeMedia(key, contentList, inputType);
}}
/>
)}
</div>
{_.map(contentList, (contentItem, index) => {
const { contentType, content, status } = contentItem;
let dom = "";
if (["init", "fail"].includes(status)) {
return (
<div className="mt12" key={index}>
<UploadingProgress
fileDesc={contentItem}
canCancelUpload
onReupload={() => this.handleReupload(contentItem)}
onAbort={() => this.handleAbort(contentItem, index)}
/>
</div>
);
}
switch (contentType) {
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 "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(contentType)
? "inline-flex"
: "flex",
}}
key={index}
>
{dom}
<span
className={
["PICTURE", "VIDEO"].includes(contentType)
? "icon_arrow iconfont"
: "icon_sider iconfont"
}
onClick={() => {
contentList.splice(index, 1);
this._onSetState();
}}
>
&#xe717;
</span>
</div>
) : null;
})}
</React.Fragment>
);
};
handleLogger = (en, cn) => {
const { onLogger } = this.props;
onLogger && onLogger(en, cn);
};
/**
* 删除选项
*
* @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.setState({ chooseOptions });
}
};
/**
* 移动选项
*
* @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.setState({ chooseOptions });
};
/**
* 重新上传
*
* @memberof QuestionInputItem
*/
handleReupload = (uploadItem) => {
uploadItem.status = "init";
Upload.uploadToOSSEvent(
uploadItem.imageFile,
uploadItem.contentName,
(url, xhr) => {
uploadItem.content = url;
uploadItem.xhr = xhr;
},
(event) => {
var percent = Math.floor((event.loaded / event.total) * 100);
uploadItem.progress = percent;
this._onSetState();
},
() => {
uploadItem.status = "success";
delete uploadItem.xhr;
delete uploadItem.progress;
delete uploadItem.imageFile;
this._onSetState();
},
() => {
uploadItem.status = "fail";
uploadItem.progress = 0;
delete uploadItem.xhr;
this._onSetState();
}
);
};
/**
* 取消上传
*
* @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 { uploadItemTarget } = this.state;
uploadItemTarget.push({
contentType: "VOICE",
type: "VOICE",
content: mp3URL,
size: duration,
});
this._onSetState({ showRecord: false });
};
/**
* 取消录制
*
* @memberof QuestionInputItem
*/
handleCancelRecord = () => {
this.setState({ showRecord: false });
};
/**
* 插入占位符
*
* @memberof QuestionInputItem
*/
insertHTML = () => {
document.getElementById("completionStem").focus();
var blanks = document.createElement("span");
blanks.className = "fill-line";
blanks.innerHTML = `&nbsp;&nbsp;填空${1}&nbsp;&nbsp;`;
blanks.contentEditable = false;
var sel, range;
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
var el = document.createElement("div");
el.appendChild(blanks);
var frag = document.createDocumentFragment(),
node,
lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
};
render() {
const {
stemContent,
chooseOptions,
judgeOptions,
analysisContent,
accept,
showRecord,
showScanFile,
scanFileType,
scanFileAddress,
blanksList,
} = this.state;
const { stemValidate, stemText, radioValidate, radioText } = this.state;
const isJudge = this.props.questionTypeKey === "JUDGE";
const isCompletion = this.props.questionTypeKey === "COMPLETION";
const placehold = isCompletion ? (
<span>
示例:党章规定,凡事有
<span className="fill-line">&nbsp;&nbsp;填空1&nbsp;&nbsp;</span>
人以上的
<span className="fill-line">&nbsp;&nbsp;填空2&nbsp;&nbsp;</span>
,都应该成立党的基层组织
</span>
) : (
"必填(1000字以内,可粘贴小图)"
);
return (
<div className="question-input-item_wrapper">
{/* 题干 */}
<Form>
<Form.Item
name="stemContent"
label="题干"
required
validateStatus={stemValidate}
help={stemText}
>
{this.renderContent(
stemContent,
placehold,
["VOICE", "RECORD", "PICTURE"],
"stem",
stemValidate
)}
</Form.Item>
{isCompletion ? (
<Form.Item
name="answer"
label={
<span>
答案{" "}
{blanksList.length === 0 && (
<span
className="icon iconfont"
style={{ color: "#BFBFBF", fontSize: 14 }}
>
&#xe7c4;
</span>
)}
</span>
}
required
className="question-answer_control"
>
{blanksList.length === 0 ? (
<span className="answer-tip">请在题干中插入答案占位符</span>
) : (
_.map(blanksList, (item, index) => {
return this.renderCompletionAnswer(item);
})
)}
</Form.Item>
) : (
<Form.Item
name="options"
label="选项"
required
className="question-option_control"
>
<div
className="question-item_options__list"
data-label="正确答案"
>
{isJudge
? _.map(judgeOptions, (optionItem, optionIndex) => {
return (
<div className="question-item_options__content">
<div className="question-item_options__setting">
<Form.Item
validateStatus={radioValidate}
help={
optionIndex === judgeOptions.length - 1
? radioText
: ""
}
>
{/* 判断 */}
<Radio
checked={optionItem.ifCorrectAnswerItem}
onClick={() => {
_.each(judgeOptions, (item) => {
item.ifCorrectAnswerItem = false;
});
optionItem.ifCorrectAnswerItem = true;
this._onSetState();
}}
/>
</Form.Item>
</div>
<div className="question-item_options__sort mr12">
{NUM_TO_WORD_MAP[optionIndex]}.
</div>
<div className="question-item_options__input">
{optionItem.label}
</div>
</div>
);
})
: _.map(chooseOptions, (optionItem, optionIndex) => {
const { itemContentVOList, questionItemVO } = optionItem;
const mediaBtn = ["VOICE", "RECORD", "PICTURE"];
const placeHold =
"必填(1000字以内,可粘贴小图;可以不输入文字,只添加音频或图片)";
return (
<div className="question-item_options__content">
<div className="question-item_options__setting">
<Form.Item
validateStatus={radioValidate}
help={
optionIndex === chooseOptions.length - 1
? radioText
: ""
}
>
{/* 单选 */}
{this.props.questionTypeKey === "THE_RADIO" && (
<Radio
checked={questionItemVO.ifCorrectAnswerItem}
onClick={() => {
_.each(chooseOptions, (o) => {
o.questionItemVO.ifCorrectAnswerItem = false;
});
questionItemVO.ifCorrectAnswerItem = true;
this._onSetState();
}}
/>
)}
{/* 多选 or 不定项 */}
{["INDEFINITE_SELECT", "MULTI_SELECT"].includes(
this.props.questionTypeKey
) && (
<Checkbox
checked={questionItemVO.ifCorrectAnswerItem}
onChange={(e) => {
const checked = e.target.checked;
questionItemVO.ifCorrectAnswerItem = 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">
<Form.Item
validateStatus={
this.state[`optionsValidate_${optionIndex}`]
}
help={this.state[`optionsText_${optionIndex}`]}
>
{this.renderContent(
itemContentVOList,
placeHold,
mediaBtn,
"options",
this.state[`optionsValidate_${optionIndex}`]
)}
</Form.Item>
</div>
<div className="question-item_options__extra">
<React.Fragment>
<span
className="option-operate_item__icon icon iconfont"
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(
analysisContent,
"1000字以内,可粘贴小图",
["VOICE", "RECORD", "PICTURE", "VIDEO"],
"analysis"
)}
</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 });
}}
/>
)}
</div>
);
}
}
export default NewQuestionTab;
.question-input-item_wrapper {
border-radius: 2px;
border: 1px solid #eeeeee;
padding: 16px;
position: relative;
margin-bottom: 50px;
.editor-fill-box_single {
border-radius: 4px;
padding: 4px 10px;
border: 1px solid #e8e8e8;
// display: flex;
// justify-content: space-between;
.fill-line {
text-decoration: underline;
}
}
.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: 70px;
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 !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;
}
.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: 11px;
transform: translateY(-100%);
font-size: 12px;
color: #666666;
line-height: 17px;
}
}
.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 {
line-height: 33px;
flex-shrink: 0;
align-self: stretch;
}
.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: 33px;
text-align: center;
// display: flex;
// justify-content: center;
// align-items: 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: 187px;
margin-left: 22px;
}
.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;
margin-right: 187px;
}
.completion-answer-box {
display: inline-flex;
width: 100%;
padding: 6px 0;
.completion-answer-label {
margin-right: 16px;
padding-top: 6px;
width: 50px;
}
.completion-answer-content {
display: flex;
background: #ffffff;
border-radius: 4px;
border: 1px solid #e8e8e8;
padding: 6px 12px;
width: calc(100% - 50px);
::-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: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-05 09:39:47
* @Description: 助学工具-题库-题库主页面侧边栏
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Input, Button, Tree } from "antd";
import "./QuestionBankSider.less";
const { Search } = Input;
const { DirectoryTree } = Tree;
class QuestionBankSider extends Component {
constructor(props) {
super(props);
this.state = {
searchValue: null,
NewEditQuestionBankCategory: null, //新增或编辑分类模态框
ImportCourseCategory: null, // 引用课程分类模态框
treeData: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023655839768500",
parentId: "0",
rootId: "1367023655839768500",
categoryLevel: 0,
categoryName: "未分类",
sort: 0,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023655839768577",
parentId: "0",
rootId: "1367023655839768577",
categoryLevel: 0,
categoryName: "分类1",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023709942095873",
parentId: "1367023655839768577",
rootId: "1367023655839768577",
categoryLevel: 1,
categoryName: "test",
sort: 0,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023725184196609",
parentId: "1367023655839768577",
rootId: "1367023655839768577",
categoryLevel: 1,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367023805563838466",
parentId: "1367023725184196609",
rootId: "1367023655839768577",
categoryLevel: 2,
categoryName: "test",
sort: 0,
},
],
sort: 1,
},
],
sort: 1,
},
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049864988516353",
parentId: "0",
rootId: "1367049864988516353",
categoryLevel: 0,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049931656978433",
parentId: "1367049864988516353",
rootId: "1367049864988516353",
categoryLevel: 1,
categoryName: "分类2",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367049973746819074",
parentId: "1367049931656978433",
rootId: "1367049864988516353",
categoryLevel: 2,
categoryName: "分类3",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367050021431861249",
parentId: "1367049973746819074",
rootId: "1367049864988516353",
categoryLevel: 3,
categoryName: "分类4",
subList: [
{
tenantId: "1122334455667788",
userId: "12345678",
source: 0,
id: "1367050063035162626",
parentId: "1367050021431861249",
rootId: "1367049864988516353",
categoryLevel: 4,
categoryName: "分类5",
sort: 0,
},
],
sort: 0,
},
],
sort: 0,
},
],
sort: 0,
},
],
sort: 2,
},
],
};
}
componentDidMount() {
this.setState({ treeData: this.renderTreeNodes(this.state.treeData) });
}
renderTreeNodes = (data, value) => {
return 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.id === "default" ? (
<span className="icon iconfont" style={{ color: "#FBD140" }}>
&#xe7f6;
</span>
) : (
<span className="icon iconfont" style={{ color: "#FBD140" }}>
&#xe7f1;
</span>
);
if (item.subList) {
item.children = this.renderTreeNodes(item.subList, value);
}
return item;
});
};
render() {
const { treeData } = this.state;
return (
<div className="question-bank-sider">
<div className="sider-title">题目分类</div>
<Search
className="sider-search"
placeholder="搜索名称分类"
onSearch={(value) => {
// TODO 调用查询分类接口
this.setState({
treeData: this.renderTreeNodes(treeData, value),
});
}}
/>
<div className="sider-btn">
<Button
onClick={() => {
window.RCHistory.push({
pathname: "/question-category-manage",
});
}}
>
分类管理
</Button>
</div>
<DirectoryTree
defaultSelectedKeys={["1367023655839768500"]}
showIcon
treeData={treeData}
/>
{treeData.length === 1 && (
<div className="empty-tree-tip">
还没有分类,需<span className="empty-tree-btn">引用课程分类</span>
哦~
</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-04 16:59:07
* @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;
}
.sider-btn {
margin-bottom: 16px;
}
.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;
.anticon {
color: #666666;
}
.ant-tree-treenode {
height: 44px;
padding: 0;
span {
line-height: 44px;
}
.ant-tree-node-content-wrapper.ant-tree-node-selected {
color: #666666;
}
}
// .ant-tree-treenode-selected:hover::before,
// .ant-tree-treenode-selected::before {
// background: rgb(255 251 240);
// }
}
}
import React, { Component } from "react";
import E from "wangeditor";
import { message } 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: "RECORD",
},
{
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,
errorInput: false,
};
}
componentDidMount() {
this.renderEditor();
}
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>`
);
this.editorRoot.change && this.editorRoot.change();
};
renderEditor() {
const { editorId } = this.state;
const { detail = {}, onChange, bindChangeContent } = this.props;
const editorRoot = new E(
`#editor${editorId}_tabbar`,
`#editor${editorId}_content`
);
editorRoot.customConfig.menus = [];
editorRoot.customConfig.uploadImgMaxSize = 1 * 1024 * 1024;
editorRoot.customConfig.customAlert = function (info) {
message.warning(/1M/.test(info) ? "图片大于1M,请使用图片上传" : info);
};
editorRoot.customConfig.customUploadImg = function (files, insert) {
// files 是 input 中选中的文件列表
// insert 是获取图片 url 后,插入到编辑器的方法
UploadOss.uploadBlobToOSS(files[0], window.random_string(16)).then(
(urlStr) => {
insert(urlStr);
}
);
};
editorRoot.customConfig.zIndex = 999;
editorRoot.customConfig.pasteFilterStyle = false;
// 自定义处理粘贴的文本内容
editorRoot.customConfig.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, " ");
if (str.length > 1000) {
str = str.substring(0, 1000);
message.error("内容过长,不能超过1000字");
}
return str;
};
editorRoot.customConfig.onchange = (html) => {
console.log(html)
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 pHeight = document.getElementById(`editor${editorId}_content`)
.firstChild.firstChild.offsetHeight;
if (pHeight > 30 || imgLength > 0) {
this.setState({ isShowSingleInput: false });
} else {
this.setState({ isShowSingleInput: true });
}
this.setState({ errorInput: contentLength > 1000 });
this.setState(
{ contentLength, visiblePlacehold: contentLength === 0 && !focusFlag },
() => {
onChange && onChange(html, this.state.contentLength);
}
);
};
editorRoot.customConfig.onblur = (html) => {
editorRoot.change && editorRoot.change();
this.setState({
focusFlag: false,
visibleMediaBox: false,
visiblePlacehold: _.isEmpty(html.replace(/\<\/?[\w]+\>/g, "")),
zIndex: 9,
});
};
editorRoot.customConfig.onfocus = () => {
this.setState({
focusFlag: true,
visibleMediaBox: true,
visiblePlacehold: false,
});
bindChangeContent && bindChangeContent(this.handleChangeContent, editorRoot.textElemId)
};
editorRoot.create();
this.editorRoot = editorRoot;
// 初始化设置内容
if (detail.content) {
const contentHtml = /^\<p/.test(detail.content)
? detail.content
: `<p>${detail.content}</p>`;
editorRoot.txt.html(contentHtml);
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, editorRoot.textElemId);
}
render() {
const {
editorId,
visiblePlacehold,
visibleMediaBox,
zIndex,
focusFlag,
contentLength,
isShowSingleInput,
errorInput,
} = this.state;
const {
placehold,
mediaBtn = ["VOICE", "RECORD", "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" ? "1px solid red" : "",
}}
>
<div
className="editor-box editor-box_content"
id={`editor${editorId}_content`}
></div>
<div className="editor-limit">
<span style={{ color: errorInput ? "red" : "" }}>
{contentLength}
</span>
/{limitLength}
</div>
</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">
<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;
.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: 17px;
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: 17px;
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: 0;
left: 0;
right: 0;
font-size: 14px;
color: #cccccc;
line-height: 22px;
margin: 6px 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.edtior-media_box {
position: absolute;
top: 100%;
left: 0;
padding-top: 9px;
&::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 10:30:52
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 10:44:46
* @Description: 助学工具-题库-题目管理主页面头部搜索
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import "./QuestionManageFilter.less";
import { Row, Input, Select, Tooltip } from "antd";
const { Search } = Input;
const DEFAULT_QUERY = {
questionName: null, // 题目名称
questionType: null, // 题型
};
class QuestionManageFilter extends Component {
constructor(props) {
super(props);
this.state = {
query: { ...DEFAULT_QUERY }, // 查询条件
questionTypeList: [], // 题型列表
};
}
render() {
const {
query: { questionName, questionType },
questionTypeList,
} = this.state;
return (
<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
value={questionName}
placeholder="搜索题目名称"
style={{ width: "calc(100% - 84px)" }}
onChange={(value) => {
console.log(value);
}}
/>
</div>
<div className="search-condition__item">
<span className="search-label">题型:</span>
<Select
value={questionType}
placeholder="请选择题目类型"
style={{ width: "calc(100% - 70px)" }}
showSearch
allowClear
filterOption={(input, option) => option}
onChange={(value) => {
console.log(value);
}}
>
{_.map(questionTypeList, (item, index) => {
return (
<Select.Option value={item.id} key={item.id}>
{item}
</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>
);
}
}
export default QuestionManageFilter;
/*
* @Author: yuananting
* @Date: 2021-02-25 10:42:51
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 11:11:10
* @Description: 助学工具-题库-题目管理主页面头部搜索样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.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: 12px;
.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;
}
/*
* @Author: yuananting
* @Date: 2021-02-25 11:23:47
* @LastEditors: yuananting
* @LastEditTime: 2021-03-04 16:55:55
* @Description: 助学工具-题库-题目管理主页面列表数据
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Table, Switch, ConfigProvider, Empty } from "antd";
import { PageControl } from "@/components";
import "./QuestionManageList.less";
class QuestionManageList extends Component {
// 表头设置
parseColumns = () => {
const columns = [
{
title: "题目",
key: "questionName",
dataIndex: "questionName",
width: 300,
render: (val, record) => {
return <div className="record-name">{val}</div>;
},
},
{
title: "题型",
key: "questionType",
dataIndex: "questionType",
},
{
title: "正确率",
key: "accuracy",
dataIndex: "accuracy",
sorter: true,
},
{
title: "更新时间",
key: "updated",
dataIndex: "updated",
sorter: true,
render: (val) => {
return formatDate("YYYY-MM-DD H:i:s", val);
},
},
{
title: "操作",
key: "operate",
dataIndex: "operate",
fixed: "right",
render: (val, record) => {
return (
<div className="record-operate">
<div
className="operate__item"
onClick={() => console.log("预览")}
>
预览
</div>
<span className="operate__item split"> | </span>
<div
className="operate__item"
onClick={() => console.log("编辑")}
>
编辑
</div>
<span className="operate__item split"> | </span>
<div
className="operate__item"
onClick={() => console.log("删除")}
>
删除
</div>
</div>
);
},
},
];
return columns;
};
// 自定义表格空状态
customizeRenderEmpty = () => {
return (
<Empty
image="https://image.xiaomaiketang.com/xm/emptyTable.png"
imageStyle={{
height: 100,
}}
description={
<div>
还没有题目,快去
<span
className="empty-list-tip"
onClick={() => {
window.RCHistory.push({
pathname: "/create-question-bank",
});
}}
>
新建一个
</span>
吧!
</div>
}
></Empty>
);
};
onShowSizeChange = (current, size) => {
if (current == size) {
return;
}
let _query = this.props.query;
_query.size = size;
this.props.onChange(_query);
};
render() {
const { dataSource = [], totalCount, query } = this.props;
const { current, size } = query;
return (
<div className="question-manage-list">
<ConfigProvider renderEmpty={this.customizeRenderEmpty}>
<Table
rowKey={(record) => record.id}
dataSource={dataSource}
columns={this.parseColumns()}
onChange={this.handleChangeTable}
pagination={false}
bordered
/>
</ConfigProvider>
{totalCount > 0 && (
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = { ...query, current: page + 1 };
this.props.onChange(_query);
}}
onShowSizeChange={this.onShowSizeChange}
/>
</div>
)}
</div>
);
}
}
export default QuestionManageList;
/*
* @Author: yuananting
* @Date: 2021-02-25 11:26:28
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 11:39:08
* @Description: 助学工具-题库-题目管理主页面列表数据样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.question-manage-list {
position: relative;
margin-top: 16px;
.empty-list-tip {
color: #ffb714;
cursor: pointer;
}
.record-name {
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
}
.record_operate {
display: flex;
&__item {
color: #5289fa;
cursor: pointer;
&.split {
margin: 0 8px;
color: #bfbfbf;
}
}
}
}
\ No newline at end of file
/*
* @Author: yuananting
* @Date: 2021-02-25 11:01:06
* @LastEditors: yuananting
* @LastEditTime: 2021-03-04 17:31:45
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Space, Button } from "antd";
class QuestionManageOpt extends Component {
handleCreateQuestionBank = () => {
window.RCHistory.push({
pathname: "/create-new-question",
});
};
render() {
return (
<Space size="large">
<Button type="primary" onClick={this.handleCreateQuestionBank}>
新建题目
</Button>
<Button
onClick={() => {
console.log("批量导入");
}}
>
批量导入
</Button>
</Space>
);
}
}
export default QuestionManageOpt;
/*
* @Author: 陈剑宇
* @Date: 2020-05-14 10:29:52
* @LastEditTime: 2021-03-02 10:11:25
* @LastEditors: yuananting
* @Description: 上传文件进度
* @FilePath: /xiaomai-web-b/app/modules/newAcademic_V5/punchClock/components/UploadingProgress.jsx
* @Copyright © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Tooltip } from 'antd';
import {
FileVerifyMap,
} from "@/common/constants/academic/lessonEnum";
import './UploadingProgress.less';
const FILE_ICON = {
WORD: 'https://image.xiaomaiketang.com/xm/5CCFBWrRMB.png',
PPT: 'https://image.xiaomaiketang.com/xm/3ypFCHEj3c.png',
EXCEL: 'https://image.xiaomaiketang.com/xm/AijpZjphPn.png',
PDF: 'https://image.xiaomaiketang.com/xm/3kZapsD3Pc.png',
VIDEO: 'https://image.xiaomaiketang.com/xm/rYCcpGaMW3.png',
VOICE: 'https://image.xiaomaiketang.com/xm/XT8eGhNhpb.png',
PICTURE: 'https://image.xiaomaiketang.com/xm/TXt5RHbFfF.png',
FAIL: 'https://image.xiaomaiketang.com/xm/EzmdwZz6mH.png'
}
const UPLOAD_FAIL = {
url: 'https://image.xiaomaiketang.com/xm/k8bynH452k.png',
title: '上传失败',
}
const UPLOAD_INIT = {
url: 'https://image.xiaomaiketang.com/xm/JbRFwhAaQ8.png',
title: '正在上传'
}
class UploadingProgress extends Component {
// 获取文件类型
getFileType(item) {
let fileEnum = 'FAIL';
if (FILE_ICON[item.contentType]) {
fileEnum = item.contentType;
} else if (FileVerifyMap[item.fileType] && FileVerifyMap[item.fileType].type) {
fileEnum = FileVerifyMap[item.fileType].type.toUpperCase()
}
return fileEnum;
}
render() {
const { fileDesc, fileDesc: { contentName, progress = 0, status = 'init' }, canCancelUpload, onAbort, onReupload } = this.props;
const isFail = status === 'fail';
const statusTips = isFail ? UPLOAD_FAIL : UPLOAD_INIT;
let imgUrl = (isFail && !canCancelUpload) ? FILE_ICON.FAIL : FILE_ICON[this.getFileType(fileDesc)];
return (
<div className="uploading-progress-box">
<div className="icon-box mr8">
<img src={imgUrl} alt="" />
</div>
<div className="file-box">
<div className="file-info">
<div className="file-title">{contentName}</div>
<div className="file-status">
<img src={statusTips.url} alt="" />
<span>{statusTips.title}</span>
</div>
</div>
{(!isFail || canCancelUpload) && <div className="file-progress-box">
<div className="file-progress" style={{ width: `${progress}%` }}></div>
</div>}
</div>
{canCancelUpload && ['init', 'fail'].includes(status) &&
<div className="file-extra-box">
<Tooltip title="取消上传"><span className="icon iconfont" onClick={() => onAbort && onAbort()} style={{transform:"scale(.8)"}}>&#xe6ef;</span></Tooltip>
{status === 'fail' && <Tooltip title='重新上传'><span className="icon iconfont" onClick={() => onReupload && onReupload()}>&#xe75a;</span></Tooltip>}
</div>
}
</div>
)
}
}
export default UploadingProgress;
\ No newline at end of file
.uploading-progress-box {
display: flex;
align-items: center;
.icon-box {
height: 44px;
width: 44px;
text-align: center;
img {
height: 100%;
}
}
.file-box {
width: 243px;
display: flex;
flex-direction: column;
justify-content: space-between;
line-height: unset;
.file- {
&info {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
&title {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
color: #666666;
line-height: 17px;
}
&status {
flex-shrink: 0;
img {
width: 14px;
height: 14px;
font-size: 0;
margin-right: 4px;
}
span {
font-size: 12px;
color: #999999;
line-height: 17px;
}
}
&progress-box {
position: relative;
height: 4px;
border-radius: 2px;
background-color: #F0F2F5;
margin-top: 10px;
}
&progress {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background-image: linear-gradient(90deg, #FBD140 0%, #FFA201 100%);
border-radius: inherit;
}
}
}
.file-extra-box {
margin-left: 40px;
.icon {
color: #BFBFBF;
font-size: 14px;
margin-left: 12px;
cursor: pointer;
}
}
}
\ No newline at end of file
/*
* @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) >= 181) {
dragDiv.style.left = "176px";
}
setPlayedTime(parseInt(dragDiv.style.left) / 180 * 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) * 180}px ` }}
/>
<div
className="flag"
style={{ left: `${(playedTime / totalTime) * 180}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;
}
.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-02 10:59:09
* @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 = () => {
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 = () => {
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 () {
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-01 17:25:37
* @LastEditors: yuananting
* @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 defineOptionData(content = '') {
return {
itemContentVOList: [{
content,
type: 'TEXT',
contentType: 'TEXT'
}], // 题目项内容
questionItemVO: { // 题目项信息
ifCorrectAnswerItem: false,
key: window.random_string(16),
}
}
}
\ No newline at end of file
/*
* @Author: yuananting
* @Date: 2021-02-23 11:43:43
* @LastEditors: yuananting
* @LastEditTime: 2021-02-25 10:32:53
* @Description: 助学工具-题库-题库引用课程分类模态框
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Modal, Tree, Card } from "antd";
import { PageControl } from "@/components";
import "./ImportCourseCategory.less";
class ImportCourseCategory extends Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 1,
},
totalCount: 0,
};
}
onSelect = (selectedKeys, info) => {
console.log("selected", selectedKeys, info);
};
onCheck = (checkedKeys, info) => {
console.log("onCheck", checkedKeys, info);
};
onShowSizeChange = (current, size) => {
if (current == size) {
return;
}
const _query = { ...this.props.query };
_query.size = size;
// 更新请求参数
this.setState({ _query });
// TODO请求
};
confirmImport = () => {
// 判断是否已经选择
// TODO save
this.props.close();
};
render() {
const treeData = [
{
title: "parent 1",
key: "0-0",
children: [
{
title: "parent 1-0",
key: "0-0-0",
disabled: true,
children: [
{
title: "leaf",
key: "0-0-0-0",
disableCheckbox: true,
},
{
title: "leaf",
key: "0-0-0-1",
},
],
},
{
title: "parent 1-1",
key: "0-0-1",
children: [
{
title: <span style={{ color: "#1890ff" }}>sss</span>,
key: "0-0-1-0",
},
],
},
],
},
];
const { query, totalCount } = this.state;
const { current, size } = query;
return (
<Modal
title="引用课程分类"
visible={true}
onOk={this.confirmImport}
onCancel={() => this.props.close()}
>
<div className="import-course-title">请选择需要的分类</div>
<Card size="small" title="分类名称">
<Tree
checkable
blockNode
height={200}
defaultExpandedKeys={["0-0-0", "0-0-1"]}
defaultSelectedKeys={["0-0-0", "0-0-1"]}
defaultCheckedKeys={["0-0-0", "0-0-1"]}
onSelect={this.onSelect}
onCheck={this.onCheck}
treeData={treeData}
/>
</Card>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = { ...query, current: page + 1 };
this.props.onChange(_query);
}}
onShowSizeChange={this.onShowSizeChange}
/>
</div>
</Modal>
);
}
}
export default ImportCourseCategory;
/*
* @Author: yuananting
* @Date: 2021-02-23 14:15:07
* @LastEditors: yuananting
* @LastEditTime: 2021-02-23 14:23:19
* @Description: 助学工具-题库-题库引用课程分类模态框样式
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
.ant-modal .ant-modal-content .ant-modal-body {
padding-top: 16px !important;
}
.import-course-title {
margin-bottom: 16px;
}
/*
* @Author: yuananting
* @Date: 2021-02-22 17:51:28
* @LastEditors: yuananting
* @LastEditTime: 2021-03-04 11:47:20
* @Description: 助学工具-题库-题库新建或编辑题库分类模态框
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
import React, { Component } from "react";
import { Modal, Form, Input, message } from "antd";
class NewEditQuestionBankCategory extends Component {
formRef = React.createRef();
constructor(props) {
super(props);
this.state = {
categoryName: null, // 分类名称
};
}
componentDidMount() {
// this.getSameLevelNodes(this.props.treeData);
}
// 确定新增或编辑
confirmOperate = async () => {
try {
await this.formRef.current.validateFields();
// TODO save
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 = [];
const { id, parentId } = this.props;
if (type === "equal") {
sameLevelNodes = this.getEqualLevelNodes(data, parentId);
} else {
sameLevelNodes = this.getChildLevelNodes(data, id);
}
return sameLevelNodes;
};
// 查询是否重名
checkExist = (sameLevelNodes, categoryName) => {
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
value={categoryName}
placeholder={`请输入${title},最多10个字`}
maxLength={10}
onChange={(e) => {
this.setState({
categoryName: e.target.value,
});
}}
/>
</Form.Item>
</Form>
</Modal>
);
}
}
export default NewEditQuestionBankCategory;
/* /*
* @Author: 吴文洁 * @Author: 吴文洁
* @Date: 2020-04-29 10:26:32 * @Date: 2020-04-29 10:26:32
* @LastEditors: wufan * @LastEditors: yuananting
* @LastEditTime: 2021-01-18 21:23:08 * @LastEditTime: 2021-03-05 09:36:18
* @Description: 内容线路由配置 * @Description: 内容线路由配置
*/ */
import EmployeesManagePage from '@/modules/store-manage/EmployeesManagePage'; import EmployeesManagePage from '@/modules/store-manage/EmployeesManagePage';
...@@ -18,6 +18,9 @@ import DataList from '@/modules/course-manage/DataList/DataList'; ...@@ -18,6 +18,9 @@ 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 QuestionBankIndex from '@/modules/teach-tool/QuestionBankIndex';
import QuestionCategoryManage from '@/modules/teach-tool/QuestionCategoryManage';
import AddNewQuestion from '@/modules/teach-tool/AddNewQuestion';
const mainRoutes = [ const mainRoutes = [
{ {
...@@ -71,6 +74,21 @@ const mainRoutes = [ ...@@ -71,6 +74,21 @@ const mainRoutes = [
name: '资料云盘' name: '资料云盘'
}, },
{ {
path: '/question-bank-index',
component:QuestionBankIndex,
name: '题库'
},
{
path: '/question-category-manage',
component:QuestionCategoryManage,
name: '分类管理'
},
{
path: '/create-new-question',
component:AddNewQuestion,
name: '新增题目'
},
{
path: '/switch-route', path: '/switch-route',
component: SwitchRoute, component: SwitchRoute,
name: '登录后跳转承载页' name: '登录后跳转承载页'
......
/*
* @Author: yuananting
* @Date: 2021-02-21 15:53:31
* @LastEditors: yuananting
* @LastEditTime: 2021-02-23 14:51:08
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
export const menuList: any = [ export const menuList: any = [
{ {
groupName: "课程管理", groupName: "课程管理",
...@@ -23,6 +31,18 @@ export const menuList: any = [ ...@@ -23,6 +31,18 @@ export const menuList: any = [
link: '/resource-disk' link: '/resource-disk'
}, },
{ {
groupName: "助学工具",
groupCode: "CloudShop",
icon: '&#xe82e;',
children: [
{
groupName: "题库",
groupCode: "ShopStaff",
link: '/question-bank-index'
}
]
},
{
groupName: "店铺管理", groupName: "店铺管理",
groupCode: "CloudShop", groupCode: "CloudShop",
icon: '&#xe82e;', icon: '&#xe82e;',
......
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