Commit 83cbc02d by guomingpang
parents 31a23f0c 9aff52ac
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
# 小麦企学院B端
## Available Scripts
小麦企学院是一个全场景企业培训数字化平台。提供了多场景解决方案,同时满足对客对内培训需求;提供了完善的服务保障体系,助力各行业持续经营进阶以及客户成功。
In the project directory, you can run:
## 目录
### `yarn start`
- [安装](#安装)
- [用法](#用法)
- [开发](#开发)
- [发布地址](#发布地址)
- [关联项目](#关联项目)
- [项目网址](#项目网址)
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
## 安装
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
```bash
git clone ssh://git@xmgit.ixm5.cn:10022/xiaomai-cloud-class/xiaomai-cloud-class-web.git
cd xiaomai-cloud-class-web
cnpm install
```
### `yarn test`
## 用法
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
```bash
npm start
```
### `yarn build`
## 开发
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
```bash
git add .
git commit -m 'commit comments'
git pull
git push
```
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
## 发布地址
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
开发环境发布地址: [http://jenkins-web.ixm5.cn/view/%E5%89%8D%E7%AB%AF/job/web-dev-%E5%B0%8F%E9%BA%A6%E4%BC%81%E5%9F%B9-cloud-class-web/](http://jenkins-web.ixm5.cn/view/%E5%89%8D%E7%AB%AF/job/web-dev-%E5%B0%8F%E9%BA%A6%E4%BC%81%E5%9F%B9-cloud-class-web/)
### `yarn eject`
正式环境发布地址:[http://jenkins-web.ixm5.cn/view/%E5%89%8D%E7%AB%AF-%E7%BA%BF%E4%B8%8A/job/web-prod-cloud-class/](http://jenkins-web.ixm5.cn/view/%E5%89%8D%E7%AB%AF-%E7%BA%BF%E4%B8%8A/job/web-prod-cloud-class/)
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
## 关联项目
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
小麦企学院c端: [https://xmgit.ixm5.cn/xiaomai-cloud-class/xiaomai-live-room](https://xmgit.ixm5.cn/xiaomai-cloud-class/xiaomai-live-room)
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
## 项目网址
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
dev环境: [https://dev.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login](https://dev.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login)
## Learn More
rc环境: [https://rc.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login](https://rc.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login)
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
灰度环境: [https://res.xiaomai5.com/xiaomai-cloud-class-web/gray/index.html#/login](https://res.xiaomai5.com/xiaomai-cloud-class-web/gray/index.html#/login)
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `yarn build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
线上环境: [https://res.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login](https://res.xiaomai5.com/xiaomai-cloud-class-web/index.html#/login)
\ No newline at end of file
......@@ -41,6 +41,7 @@
"dom-to-image": "^2.6.0",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"download": "^8.0.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "3.0.3",
......
......@@ -19,7 +19,7 @@ class ChooseMembersModal extends React.Component {
this.state = {
allUserList : [], // 所有成员列表
selectUserList: [], // 已选则成员
temporaryList: [], // 临时用户成员列表(搜索时使用)
temporaryList: [], // 临时学员成员列表(搜索时使用)
instId : window.currentUserInstInfo.instId, // 机构Id
searchKey : null, // 搜索内容
selectedRowKeys : [], // 勾选的成员
......@@ -229,7 +229,7 @@ class ChooseMembersModal extends React.Component {
selectedColumnsRight = () => {
const selectColumns = [
{
title: '用户名',
title: '学员名',
key: 'adminNameRight',
dataIndex: 'adminName',
width: '70%',
......
/*
* @Author: 吴文洁
* @Date: 2020-08-31 09:34:25
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-04-13 17:56:34
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-05-12 17:27:08
* @Description:
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -101,7 +101,12 @@ class User {
setCustomerStoreId(value:any) {
return Storage.set(`${PREFIX}_customerStoreId`,value);
}
setIdentifier(value:any){
return Storage.set(`${PREFIX}_identifier`,value);
}
getIdentifier(){
return Storage.get(`${PREFIX}_identifier`);
}
clearUserInfo(){
Storage.remove(`${USER_PREFIX}_token`);
Storage.remove(`${USER_PREFIX}_userId`);
......
......@@ -115,7 +115,7 @@ class MultipleDatePicker extends React.Component {
}
handleShowDetail(time, className) {
if(!this.props.canSelectTodayBefore && className === 'before-disabled') {
if((!this.props.canSelectTodayBefore && className === 'before-disabled') || this.props.disabled) {
return
}
let date = time.valueOf();
......
/*
* @Author: sunbingqing
* @Date: 2019-07-24 17:21:00
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-25 14:36:16
* @Last Modified by: chenshu
* @Last Modified time: 2021-05-25 15:14:55
*/
import React from 'react';
......@@ -43,7 +43,7 @@ const PageControl = (props: PageControlProps) => {
total={total || 0}
pageSizeOptions={['10', '20', '50', '100']}
onShowSizeChange={onShowSizeChange}
showSizeChanger={showSizeChanger}
showSizeChanger={!!showSizeChanger}
pageSize={pageSize || 10}
onChange={(page: any) => _onChange(page)}
showQuickJumper
......
......@@ -6,6 +6,7 @@
import domtoimage from 'dom-to-image'
import { Popover } from 'antd'
import React from 'react'
import $ from 'jquery';
// 时间控件优化方法
window.setCorrectDate = date => {
......@@ -958,17 +959,8 @@ window.copyText = textContent => {
}
window.downloadFile = (dataURL, fileName) => {
const eleDom = document.createElement('a')
const blob = window.convertBase64ToBlob(dataURL)
const href = window.getObjectURL(blob)
$(eleDom)
.attr({
href,
download: fileName
})
.get(0)
.click()
window.download(blob, fileName, 'image/png');
}
window.getCouponRule = record => {
......
/*
* @Author: wufan
* @Date: 2020-11-25 18:25:02
* @LastEditors: zangsuyun
* @LastEditTime: 2021-03-22 13:58:04
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:25:01
* @Description: Description
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -23,7 +23,7 @@ export default class courseService {
return getQrcode(params);
}
// 获取用户上课数据
// 获取学员上课数据
static fetchUserData(params: any) {
return fetchUserData(params);
}
......
......@@ -25,7 +25,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="//at.alicdn.com/t/font_2223403_vs61tfrogho.css">
<link rel="stylesheet" href="//at.alicdn.com/t/font_2223403_e0vkqcd8igi.css">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
......
......@@ -23,7 +23,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="//at.alicdn.com/t/font_2223403_vs61tfrogho.css">
<link rel="stylesheet" href="//at.alicdn.com/t/font_2223403_e0vkqcd8igi.css">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
......@@ -55,5 +55,160 @@
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.download = factory();
}
}(this, function () {
return function download(data, strFileName, strMimeType) {
var self = window, // this script is only for browsers anyway...
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
mimeType = strMimeType || defaultMime,
payload = data,
url = !strFileName && !strMimeType && payload,
anchor = document.createElement("a"),
toString = function(a){return String(a);},
myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString),
fileName = strFileName || "download",
blob,
reader;
myBlob= myBlob.call ? myBlob.bind(self) : Blob ;
if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
payload=[payload, mimeType];
mimeType=payload[0];
payload=payload[1];
}
if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument
fileName = url.split("/").pop().split("?")[0];
anchor.href = url; // assign href prop to temp anchor
if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path:
var ajax=new XMLHttpRequest();
ajax.open( "GET", url, true);
ajax.responseType = 'blob';
ajax.onload= function(e){
download(e.target.response, fileName, defaultMime);
};
setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return:
return ajax;
} // end if valid url?
} // end if url?
//go ahead and download dataURLs right away
if(/^data\:[\w+\-]+\/[\w+\-]+[,;]/.test(payload)){
if(payload.length > (1024*1024*1.999) && myBlob !== toString ){
payload=dataUrlToBlob(payload);
mimeType=payload.type || defaultMime;
}else{
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
navigator.msSaveBlob(dataUrlToBlob(payload), fileName) :
saver(payload) ; // everyone else can save dataURLs un-processed
}
}//end if dataURL passed?
blob = payload instanceof myBlob ?
payload :
new myBlob([payload], {type: mimeType}) ;
function dataUrlToBlob(strUrl) {
var parts= strUrl.split(/[:;,]/),
type= parts[1],
decoder= parts[2] == "base64" ? atob : decodeURIComponent,
binData= decoder( parts.pop() ),
mx= binData.length,
i= 0,
uiArr= new Uint8Array(mx);
for(i;i<mx;++i) uiArr[i]= binData.charCodeAt(i);
return new myBlob([uiArr], {type: type});
}
function saver(url, winMode){
if ('download' in anchor) { //html5 A[download]
anchor.href = url;
anchor.setAttribute("download", fileName);
anchor.className = "download-js-link";
anchor.innerHTML = "downloading...";
anchor.style.display = "none";
document.body.appendChild(anchor);
setTimeout(function() {
anchor.click();
document.body.removeChild(anchor);
if(winMode===true){setTimeout(function(){ self.URL.revokeObjectURL(anchor.href);}, 250 );}
}, 66);
return true;
}
// handle non-a[download] safari as best we can:
if(/(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test(navigator.userAgent)) {
url=url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
if(!window.open(url)){ // popup blocked, offer direct download:
if(confirm("Displaying New Document\n\nUse Save As... to download, then click back to return to this page.")){ location.href=url; }
}
return true;
}
//do iframe dataURL download (old ch+FF):
var f = document.createElement("iframe");
document.body.appendChild(f);
if(!winMode){ // force a mime that will download:
url="data:"+url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
}
f.src=url;
setTimeout(function(){ document.body.removeChild(f); }, 333);
}//end saver
if (navigator.msSaveBlob) { // IE10+ : (has Blob, but not a[download] or URL)
return navigator.msSaveBlob(blob, fileName);
}
if(self.URL){ // simple fast and modern way using Blob and URL:
saver(self.URL.createObjectURL(blob), true);
}else{
// handle non-Blob()+non-URL browsers:
if(typeof blob === "string" || blob.constructor===toString ){
try{
return saver( "data:" + mimeType + ";base64," + self.btoa(blob) );
}catch(y){
return saver( "data:" + mimeType + "," + encodeURIComponent(blob) );
}
}
// Blob but not URL support:
reader=new FileReader();
reader.onload=function(e){
saver(this.result);
};
reader.readAsDataURL(blob);
}
return true;
}; /* end download() */
}))
</script>
</body>
</html>
/*
* @Author: wufan
* @Date: 2020-11-30 10:47:38
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-02-02 10:22:34
* @Description: 用户管理页面
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 17:45:53
* @Description: 学员管理页面
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -60,7 +60,7 @@ function UserManagePage() {
function parseColumn() {
const list = [
{
title: "用户姓名",
title: "学员姓名",
dataIndex: "nickName",
render: (val: string, item: any) => {
return (
......@@ -117,16 +117,16 @@ function UserManagePage() {
return (
<div className=" page user-manage-page">
<div className="content-header">用户管理</div>
<div className="content-header">学员管理</div>
<div className="box">
<div className="box-header">
<div className="header-item">
<span className="item-name">搜索用户</span>
<span className="item-name">搜索学员</span>
<Search
style={{
width: 300,
}}
placeholder="搜索用户姓名/手机号"
placeholder="搜索学员姓名/手机号"
onSearch={(value) => {
const _query = { ...query };
if (value) {
......@@ -149,7 +149,7 @@ function UserManagePage() {
</div>
<div className="header-item">
<span className="item-name">用户来源:</span>
<span className="item-name">学员来源:</span>
<Select
style={{ width: '100%' }}
placeholder="请选择"
......@@ -195,7 +195,7 @@ function UserManagePage() {
onClick={() => {
handleToAddEmployee();
}}
>添加用户</Button>
>添加学员</Button>
}
<div className="box-body">
<Table
......
......@@ -21,7 +21,7 @@ class ChooseMembersModal extends React.Component {
isOpen: props.isOpen,
allUserList : [], // 所有成员列表
selectUserList: [], // 已选则成员
temporaryList: [], // 临时用户成员列表(搜索时使用)
temporaryList: [], // 临时学员成员列表(搜索时使用)
instId : window.currentUserInstInfo.instId, // 机构Id
searchKey : null, // 搜索内容
selectedRowKeys : [], // 勾选的成员
......@@ -230,7 +230,7 @@ class ChooseMembersModal extends React.Component {
selectedColumnsRight = () => {
const selectColumns = [
{
title: '用户名',
title: '学员名',
key: 'nameRight',
dataIndex: 'name',
width: '70%',
......@@ -274,7 +274,7 @@ class ChooseMembersModal extends React.Component {
isOpen,
selectObject,
} = this.state;
const title = type === 'USER' ? '添加员工' : '添加用户';
const title = type === 'USER' ? '添加员工' : '添加学员';
return (
<div>
{/* 添加学员页面 */}
......@@ -284,7 +284,7 @@ class ChooseMembersModal extends React.Component {
onCancel={() => this.handleClose()}
onOk={() => {
if (_.isEmpty(selectUserList)) {
message.warning(type === 'USER' ? '请选择员工' : '请选择用户')
message.warning(type === 'USER' ? '请选择员工' : '请选择学员')
return null;
}
type === 'USER' ? this.setState({ openSetModal: true, isOpen: false }) : this.addCustomer();
......@@ -341,7 +341,7 @@ class ChooseMembersModal extends React.Component {
</div>
{/* 已选择的成员列表 */}
<div className='container-right'>
<span className='span-left'>已选择{type === 'USER' ? '员工' : '用户'}</span>
<span className='span-left'>已选择{type === 'USER' ? '员工' : '学员'}</span>
<div className='span-right' onClick={() => this.clearAllUser()}>
<span className={ (selectUserList.length > 0) ? 'span-right-l' : null }>清空</span>
</div>
......
......@@ -88,7 +88,7 @@ export default class SetEmployeeModal extends React.Component {
<Radio value={"Cloud_Operator"} className="mt-4">
<span style={{ color: "#333" }}>运营师</span>
<p className="radio-tip">
仅可查看/转发培训计划内容,并查看负责的用户学习进度
仅可查看/转发培训计划内容,并查看负责的学员学习进度
</p>
</Radio>
<Radio value={"Cloud_Lecturer"} className="mt-4">
......
......@@ -16,7 +16,6 @@ class DateRangePicker extends React.Component {
const showTime = { showTime: false }
return (
<RangePicker
{...this.props}
format={this.props.format || 'YYYY-MM-DD'}
allowClear={this.props.allowClear}
onChange={(date) => {
......@@ -28,6 +27,7 @@ class DateRangePicker extends React.Component {
}}
{...showTime}
suffixIcon={<span className="icon iconfont">&#xe838;</span>}
{...this.props}
/>
)
}
......
......@@ -106,7 +106,7 @@ class DataList extends React.Component {
getStudentColumns() {
const columns = [
{
title: "用户姓名",
title: "学员姓名",
dataIndex: "userName",
},
{
......@@ -304,9 +304,9 @@ class DataList extends React.Component {
</div>
</div>
{/* 用户上课数据 */}
{/* 学员上课数据 */}
<div className="courseData-student">
<p className="title">用户上课数据</p>
<p className="title">学员上课数据</p>
<div className="filter-wrap">
<div className="filter">
<Button
......
......@@ -56,7 +56,7 @@ class PlaybackData extends React.Component {
getPlaybackColumns() {
const columns = [
{
title: "观看用户",
title: "观看学员",
dataIndex: "userName",
},
{
......
......@@ -132,8 +132,8 @@ class AddLiveIntro extends React.Component {
</div>
<div>
<div class="instro-text">
<div>开启:允许未绑定手机号的用户进入直播间观看直播</div>
<div>关闭:仅限绑定了手机号的用户可以进入直播间观看直播</div>
<div>开启:允许未绑定手机号的学员进入直播间观看直播</div>
<div>关闭:仅限绑定了手机号的学员可以进入直播间观看直播</div>
</div>
</div>
</div>
......
......@@ -15,16 +15,15 @@ class GraphicsEditor extends React.Component {
textLength: 0,
showSelectImageModal: false,
showSelectVideoModal: false,
diskList: [],
}
this.editorInt = null;
this.isContent = true;
}
componentDidMount() {
this.renderEditor()
this.resetIndex(true);
this.initBus();
this.bindClick();
}
......@@ -32,23 +31,6 @@ class GraphicsEditor extends React.Component {
componentWillUnmount() {
this.resetIndex();
this.removeBus();
this.removeClick();
}
bindClick = () => {
window.addEventListener('click', this.clickEditor)
}
removeClick = () => {
window.removeEventListener('click', this.clickEditor)
}
clickEditor = (e) => {
if (e && e.target.closest('.content-editor')) {
this.isContent = true
} else if (e && e.target.closest('.introduce-editor')) {
this.isContent = false
}
}
resetIndex = (bool) => {
......@@ -102,9 +84,10 @@ class GraphicsEditor extends React.Component {
}
this.editorInt = new E(`#editor${editorId}`);
this.editorInt.config.focus = false;
this.editorInt.config.showFullScreen = !isIntro
this.editorInt.menus.extend('xmimage', ImageMenu);
this.editorInt.menus.extend('xmvideo', VideoMenu);
!isIntro && this.editorInt.menus.extend('xmvideo', VideoMenu);
this.editorInt.config.menus = isIntro ?
[
'head',
......@@ -184,19 +167,21 @@ class GraphicsEditor extends React.Component {
// 选择图文
handleSelectVideo = (file) => {
const { ossUrl } = file || {};
if (!ossUrl) return null;
this.setState({
showSelectVideoModal: false
})
const { ossUrl } = file;
const { detail } = this.props;
this.editorInt && this.editorInt.txt.html(`${detail.content}<p style="width: 100%;padding-top: 56.25%;position: relative;"><iframe style="position: absolute;width: 100%;height: 100%;top: 0;left: 0;" src="${ossUrl}"></iframe><br/></p><p><br/></p>`)
}
handleSelectImage = (file) => {
const { ossUrl } = file || {};
if (!ossUrl) return null;
this.setState({
showSelectImageModal: false
})
const { ossUrl } = file;
const { detail } = this.props;
this.editorInt && this.editorInt.txt.html(`${detail.content}<p><img style="max-width: 100%;" src="${ossUrl}" /><br/><p>`)
}
......@@ -204,13 +189,13 @@ class GraphicsEditor extends React.Component {
initBus = () => {
const { isIntro } = this.props;
Bus.bind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage)
Bus.bind('graphicsEditorVideo', this.uploadVideo)
!isIntro && Bus.bind('graphicsEditorVideo', this.uploadVideo)
}
removeBus = () => {
const { isIntro } = this.props;
Bus.unbind(`graphicsEditorImage${isIntro ? '' : 'Content'}`, this.uploadImage)
Bus.unbind('graphicsEditorVideo', this.uploadVideo)
!isIntro && Bus.unbind('graphicsEditorVideo', this.uploadVideo)
}
uploadImage = () => {
......@@ -222,7 +207,13 @@ class GraphicsEditor extends React.Component {
}
render() {
const { editorId, textLength, showSelectImageModal, showSelectVideoModal } = this.state;
const {
editorId,
textLength,
showSelectImageModal,
showSelectVideoModal,
diskList,
} = this.state;
const { limitLength = 1000, isIntro, maxLimit } = this.props;
return <div className={`graphics-editor-container${isIntro ? ' introduce' : ''} ${(textLength > maxLimit)&& 'warning'}`}>
<div className="editor-box" id={`editor${editorId}`} ></div>
......
......@@ -262,7 +262,7 @@ class LiveCourseList extends React.Component {
{
title: <span>
<span>学院展示</span>
<Tooltip title={<div>开启后,用户可在学院内查看到此课程。若课程“未成功开课”,则系统会自动“关闭”学院展示。<br/>关闭后,学院内不再展示此课程,但用户仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
<Tooltip title={<div>开启后,学员可在学院内查看到此课程。若课程“未成功开课”,则系统会自动“关闭”学院展示。<br/>关闭后,学院内不再展示此课程,但学员仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
width: "9%",
key: "shelfState",
......@@ -580,7 +580,7 @@ class LiveCourseList extends React.Component {
handleDelete = (record)=>{
return confirm({
title: '你确定要删除直播课?',
content: '删除后,用户将不能观看直播课/回放',
content: '删除后,学员将不能观看直播课/回放',
icon: <span className="icon iconfont default-confirm-icon">&#xe839; </span>,
okText: '删除',
okType: 'danger',
......@@ -658,7 +658,7 @@ class LiveCourseList extends React.Component {
}
getDownloadVersion() {
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
// 判断用户系统
// 判断学员系统
let platform;
if(!isMac){
platform = 1
......
......@@ -28,7 +28,7 @@ class LiveCourseOpt extends React.Component {
// 下载直播客户端
handleDownloadClient = () => {
const { isMac } = this.state;
// 判断用户系统
// 判断学员系统
let platform;
if(!isMac){
platform = 1
......
......@@ -56,7 +56,6 @@ class AddGraphicsCourse extends React.Component {
coverUrl: defaultCoverUrl, // 图文课封面
studentList: [], // 上课学员列表
shelfState:'YES', //是否开启学院展示
diskList: [], // 机构可见磁盘目录
selectedFileList: [], // 已经从资料云盘中勾选的文件
showCutModal: false, // 是否显示截图弹窗
showSelectVideoModal: false,
......@@ -65,7 +64,6 @@ class AddGraphicsCourse extends React.Component {
courseCatalogList:[], //分类列表
categoryId:null, //分类的Id值
whetherVisitorsJoin: 'NO', // 是否允许游客加入
isContent: true,
}
}
......@@ -513,7 +511,6 @@ class AddGraphicsCourse extends React.Component {
courseMedia,
introduce,
showCutModal,
diskList,
imageFile,
videoType,
shelfState,
......
......@@ -118,8 +118,8 @@ class AddGraphicsIntro extends React.Component {
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的用户观看</div>
<div>关闭:仅限绑定了手机号的用户可以进入观看图文课</div>
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看图文课</div>
</div>
</div>
</div>
......@@ -133,8 +133,8 @@ class AddGraphicsIntro extends React.Component {
</Col>
<Col span={21}>
<div className="desc">
<div>开启:图文课将在用户学院图文课列表中展示</div>
<div>关闭:图文课将在用户学院图文课列表中隐藏</div>
<div>开启:图文课将在学员学院图文课列表中展示</div>
<div>关闭:图文课将在学员学院图文课列表中隐藏</div>
</div>
</Col>
</Row>
......
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-27 16:24:47
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:32:38
* @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -137,7 +137,7 @@ class GraphicsCourseList extends React.Component {
{
title: <span>
<span>学院展示</span>
<Tooltip title={<div>开启后,用户可在学院内查看到此课程。若课程“未成功开课”,则系统会自动“关闭”学院展示。<br/>关闭后,学院内不再展示此课程,但用户仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
<Tooltip title={<div>开启后,学员可在学院内查看到此课程。<br/>关闭后,学院内不再展示此课程,但学员仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
width: 120,
dataIndex: "courseware",
......@@ -148,7 +148,7 @@ class GraphicsCourseList extends React.Component {
},
},
{
title: "观看用户数",
title: "观看学员数",
width: 110,
key: "watchUserCount",
dataIndex: "watchUserCount",
......
......@@ -84,7 +84,7 @@ class WatchDataModal extends React.Component {
parseColumns = () => {
const columns = [
{
title: '观看用户',
title: '观看学员',
key: 'name',
dataIndex: 'name'
},
......@@ -141,7 +141,7 @@ class WatchDataModal extends React.Component {
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
>
<div className="search-container">
<Search placeholder="搜索用户姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
<Search placeholder="搜索学员姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
</div>
<div>
<Table
......
......@@ -2,7 +2,7 @@
* @Author: 吴文洁
* @Date: 2020-07-20 19:12:49
* @Last Modified by: chenshu
* @Last Modified time: 2021-03-16 17:41:40
* @Last Modified time: 2021-05-25 16:11:25
* @Description: 大班直播分享弹窗
*/
......@@ -105,7 +105,8 @@ class ShareLiveModal extends React.Component {
showImg:true,
time:new Date().valueOf()
},()=>{
this.setState({time:new Date().valueOf()},()=>{
this.setState({ time: new Date().valueOf() }, () => {
setTimeout(() => {
let node = document.getElementById('poster');
domtoimage.toPng(node)
.then((imgData) => {
......@@ -115,6 +116,7 @@ class ShareLiveModal extends React.Component {
$(download).attr('href', imgData).attr('download', `${courseName}.png`).get(0).click();
// this.props.close()
})
}, 1000)
})
})
......@@ -139,7 +141,7 @@ class ShareLiveModal extends React.Component {
let coverImgSrc = coverUrl;
if(type === 'videoClass'){
if((!coverUrl || isDefaultCover) && title !== '图文课'){
if((!coverUrl || isDefaultCover) && title !== '图文课' && title != '线下课'){
coverImgSrc = `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring`
}
}else{
......@@ -197,22 +199,22 @@ class ShareLiveModal extends React.Component {
<div className="share-poster right__item">
<div className="title">① 海报分享</div>
{ type === "liveClass" &&
<div className="sub-title">用户可通过微信扫描海报二维码,观看{title}</div>
<div className="sub-title">学员可通过微信扫描海报二维码,观看{title}</div>
}
{ type === "videoClass" &&
<div className="sub-title">用户可通过微信识别二维码,报名观看{title}</div>
<div className="sub-title">学员可通过微信识别二维码,报名观看{title}</div>
}
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
<div className="content" onClick={_.debounce(this.handleDownloadPoster, 1000, true)}>下载海报</div>
</div>
<div className="share-url right__item">
<div className="title">② 链接分享</div>
{ type === "liveClass" &&
<div className="sub-title">用户可通过微信打开以下链接,观看{title}</div>
<div className="sub-title">学员可通过微信打开以下链接,观看{title}</div>
}
{ type === "videoClass" &&
<div className="sub-title">用户可通过打开链接,报名观看{title}</div>
<div className="sub-title">学员可通过打开链接,报名观看{title}</div>
}
<div className="content url-content">
<div className="share-url" id="shareUrl">{shareUrl}</div>
......
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:07:47
* @LastEditors: wufan
* @LastEditTime: 2021-05-25 20:00:18
* @Description: 线下课新增/编辑页
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import {
Button,
Input,
Radio,
message,
Modal,
TreeSelect,
Select,
Switch,
TimePicker,
InputNumber,
Tooltip,
} from 'antd';
import $ from 'jquery';
import RangePicker from "@/modules/common/DateRangePicker";
import ShowTips from "@/components/ShowTips";
import Breadcrumbs from "@/components/Breadcrumbs";
import SelectStudent from '../modal/select-student';
import SelectPrepareFileModal from '../../prepare-lesson/modal/SelectPrepareFileModal';
import PreviewOfflineModal from './modal/PreviewOfflineModal';
import StoreService from "@/domains/store-domain/storeService";
import Service from '@/common/js/service';
import { randomString } from '@/domains/basic-domain/utils';
import User from '@/common/js/user';
import _ from "underscore";
import moment from 'moment';
import Upload from '@/core/upload';
import GraphicsEditor from '../components/GraphicsEditor';
import MultipleDatePicker from '@/components/MultipleDatePicker';
import './AddOfflineCourse.less';
const { Option } = Select;
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
let cutFlag = false;
const unitList = [
{ key: 'HOUR', value: '小时' },
{ key: 'MINUTE', value: '分钟' },
]
class AddOfflineCourse extends React.Component {
constructor(props) {
super(props);
const courseId = getParameterByName("id");
const pageType = getParameterByName("type");
this.state = {
courseId, // 线下课ID,编辑的时候从URL上带过来
pageType, // 页面类型: add->新建 edit->编辑
imageFile: null, // 需要被截取的图片
courseName: null, // 线下课名称
courseMedia: '',
introduce: '',
coverId: null, // 线下封面的recourceId
coverUrl: defaultCoverUrl, // 线下课封面
studentList: [], // 上课学员列表
diskList: [], // 机构可见磁盘目录
selectedFileList: [], // 已经从资料云盘中勾选的文件
showCutModal: false, // 是否显示截图弹窗
studentModal: false,
categoryName:null, //分类名称
categoryList: [],
courseCatalogList:[], //分类列表
categoryId:null, //分类的Id值
whetherVisitorsJoin: 'NO', // 是否允许游客加入
isContent: true,
teacherList: [],
teacherQuery: {
size: 15,
current: 1,
nickName:null
},
calendarTime: [],
offlineCourseType: 'ALL_DAY_OFFLINE',
signInType: 'START_AGO',
signOutType: 'START_LATER',
oldQuta: null,
quota: null,
signInTimeNum: null,
signOutEndTimeNum: null,
signOutStartTimeNum: null,
signInTimeUnit: 'MINUTE',
signOutStartTimeUnit: 'MINUTE',
signOutEndTimeUnit: 'MINUTE',
whetherSetApply: 'YES',
whetherSetSignIn: 'YES',
whetherSetSignOut: 'YES',
isEditDisablie: false,
startTime: new Date().getTime() + 300000, // 批量开始时分
endTime: new Date().getTime() + 300000, // 批量结束时分
}
}
componentWillMount() {
const { courseId, pageType } = this.state;
this.getCourseCatalogList();
this.getTeacherList();
if (pageType === 'edit') {
this.handleFetchScheudleDetail(courseId);
}
}
initBus = () => {
Bus.bind('offlineEditorImage', this.uploadImage)
}
removeBus = () => {
Bus.unbind('offlineEditorImage', this.uploadImage)
}
uploadImage = () => {
this.setState({ showSelectImageModal: true })
}
//获取分类列表
getCourseCatalogList = ()=>{
Service.Hades('public/hades/queryCategoryTree', { source: 0, tenantId: User.getStoreId(), count: false, userId: User.getUserId() }).then((res) => {
const { categoryList = [] } = res.result;
this.setState({
categoryList,
courseCatalogList: this.renderTreeNodes(categoryList),
})
});
}
renderTreeNodes = (data) => {
let newTreeData = data.map((item) => {
item.title = item.categoryName;
item.value = item.id;
item.key = item.id;
if (item.sonCategoryList) {
item.children = this.renderTreeNodes(item.sonCategoryList);
}
return item;
});
return newTreeData;
};
checkDetail = (courseId) => {
return Service.Hades('public/hades/getOfflineCourseDetail', {
courseId
}).then((res) => {
const { courseState } = res.result;
return courseState === 'UN_START';
});
}
// 获取线下课详情
handleFetchScheudleDetail = (courseId) => {
return Service.Hades('public/hades/getOfflineCourseDetail',{
courseId
}).then((res) => {
const { result = {} } = res || {};
const {
courseName,
courseState,
categoryId,
offlinePlace,
whetherVisitorsJoin,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
teacherId,
teacherName,
offlineCourseType,
startTime,
endTime,
startTimeApply,
endTimeApply,
quota,
calendarTime,
courseMediaVOS,
categoryName,
signInTimeNum,
signInTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
signInType,
signOutType,
whetherHaveApply,
} = result;
let coverId;
let coverUrl = this.state.coverUrl;
let hasIntro = false;
courseMediaVOS.map((item) => {
switch (item.contentType){
case "COVER":
coverId = item.mediaContent;
coverUrl = item.mediaUrl;
break;
case "SCHEDULE":
this.getTextDetail('courseMedia', item.mediaUrl);
break;
case "INTRO":
hasIntro = true;
this.getTextDetail('introduce', item.mediaUrl);
break;
default:
break;
}
return item;
})
this.setState({
loadintroduce: !hasIntro,
coverId,
coverUrl,
categoryName,
courseName,
categoryId,
offlinePlace,
whetherVisitorsJoin,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
teacherId,
teacherName,
offlineCourseType,
startTime,
endTime,
startTimeApply,
endTimeApply,
quota: quota < 0 ? null : quota,
oldQuta: quota,
calendarTime,
signInTimeNum,
signInTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
signInType,
signOutType,
isEditDisablie: whetherHaveApply === 'YES',
});
})
}
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ [key]: res, [`load${key}`]: true });
}
})
}
handleGoBack = () => {
const {
coverId,
videoName,
videoDuration,
courseName,
categoryId,
whetherVisitorsJoin
} = this.state;
if(videoName || videoDuration || categoryId || courseName || coverId || whetherVisitorsJoin !== whetherVisitorsJoin ){
Modal.confirm({
title: '确认要返回吗?',
content: '返回后,本次编辑的内容将不被保存。',
okText: '确认返回',
cancelText: '留在本页',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
onOk: () => {
window.RCHistory.push({
pathname: `/offline-course`,
});
}
});
}else{
window.RCHistory.push({
pathname: `/offline-course`,
});
}
}
// 显示预览弹窗
handleShowPreviewModal = () => {
const {
coverUrl,
courseName,
teacherName,
startTime,
endTime,
introduce,
categoryName,
offlinePlace,
whetherSetApply,
startTimeApply,
endTimeApply,
whetherSetSignIn,
whetherSetSignOut,
signInType,
signOutType,
signInTimeNum,
signInTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
} = this.state;
const data = {
coverUrl,
courseName,
teacherName,
startTime,
endTime,
categoryName,
introduce,
offlinePlace,
whetherSetApply,
startTimeApply,
endTimeApply,
whetherSetSignIn,
whetherSetSignOut,
signInType,
signOutType,
signInTimeNum,
signInTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
}
const previewOfflineModal = (
<PreviewOfflineModal
data={data}
close={() => {
this.setState({
previewOfflineModal: null
})
}}
/>
);
this.setState({ previewOfflineModal });
}
handleSelectCover = (file)=> {
this.uploadCoverImage(file);
}
//上传图片
uploadCoverImage = (imageFile) => {
const { folderName } = imageFile;
const fileName = window.random_string(16) + folderName.slice(folderName.lastIndexOf("."));
const self = this;
this.setState(
{
visible: true,
},
() => {
setTimeout(() => {
const okBtnDom = document.querySelector("#headPicModal");
const options = {
size: [500, 282],
ok: okBtnDom,
maxZoom: 3,
style: {
jpgFillColor: "transparent",
},
done: function (dataUrl) {
clearTimeout(self.timer);
self.timer = setTimeout(() => {
if ((self.state.rotate != this.rotate()) || (self.state.scale != this.scale())) {
const _dataUrl = this.clip()
const cutImageBlob = self.convertBase64UrlToBlob(_dataUrl);
self.setState({
cutImageBlob,
dataUrl: _dataUrl,
rotate: this.rotate(),
scale: this.scale()
})
}
}, 500)
const cutImageBlob = self.convertBase64UrlToBlob(dataUrl);
self.setState({
cutImageBlob,
dataUrl
})
setTimeout(() => {
cutFlag = false;
}, 2000);
},
fail: (failInfo) => {
message.error("图片上传失败了,请重新上传");
},
loadComplete: function (img) {
setTimeout(() => {
const _dataUrl = this.clip()
self.setState({
dataUrl: _dataUrl,
hasImgReady: true
})
}, 100)
},
};
const imgUrl = `${imageFile.ossUrl}?${new Date().getTime()}`
if (!this.state.photoclip) {
const _photoclip = new PhotoClip("#headPicModal", options);
_photoclip.load(imgUrl);
this.setState({
photoclip: _photoclip,
});
} else {
this.state.photoclip.clear();
this.state.photoclip.load(imgUrl);
}
}, 200);
}
);
};
//获取resourceId
getSignature = (blob, fileName) => {
Upload.uploadBlobToOSS(blob, 'cover' + (new Date()).valueOf(),null,'signInfo').then((signInfo) => {
this.setState({
coverClicpPath:signInfo.fileUrl,
coverId:signInfo.resourceId,
visible: false
},()=>this.updateCover())
});
};
updateCover = () =>{
const {coverClicpPath,coverId} = this.state
this.setState({
showSelectCoverModal: false,
coverUrl:coverClicpPath,
coverId:coverId
})
}
// base64转换成blob
convertBase64UrlToBlob = (urlData) => {
const bytes = window.atob(urlData.split(",")[1]);
const ab = new ArrayBuffer(bytes.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: "image/png" });
};
preSubmit = () => {
const { courseId } = this.state;
if (courseId) {
this.checkDetail(courseId).then(bool => bool ? this.handleSubmit() : message.warning('课程已开始,无法继续编辑'))
} else {
this.handleSubmit();
}
}
// 保存
handleSubmit = () => {
const {
courseId,
coverId,
pageType,
courseName,
introduce,
categoryId,
offlinePlace,
whetherVisitorsJoin,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
teacherId,
coverUrl,
offlineCourseType,
startTime,
endTime,
startTimeApply,
endTimeApply,
quota,
calendarTime,
signInType,
signInTimeNum,
signInTimeUnit,
signOutType,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
isMore,
} = this.state;
let coverObj ={
contentType:'COVER',
mediaContent: coverId,
mediaType:'PICTURE',
mediaUrl: coverUrl,
}
let scheduleMediaRequests = [];
if(coverId){
scheduleMediaRequests = [coverObj]
}
const commonParams = {
categoryId,
courseName,
scheduleMediaRequests,
storeId: User.getStoreId(),
offlinePlace,
whetherVisitorsJoin,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
teacherId,
offlineCourseType,
startTime,
endTime,
calendarTime,
isMore,
};
if (whetherSetApply === 'YES') {
commonParams.startTimeApply = startTimeApply;
commonParams.endTimeApply = endTimeApply;
commonParams.quota = quota;
}
if (whetherSetSignIn === 'YES') {
commonParams.signInType = signInType;
commonParams.signInTimeNum = signInTimeNum;
commonParams.signInTimeUnit = signInTimeUnit;
}
if (whetherSetSignOut === 'YES') {
commonParams.signOutType = signOutType;
if (commonParams.signOutType === 'START_LATER') {
commonParams.signOutStartTimeNum = signOutStartTimeNum;
commonParams.signOutStartTimeUnit = signOutStartTimeUnit;
}
commonParams.signOutEndTimeNum = signOutEndTimeNum;
commonParams.signOutEndTimeUnit = signOutEndTimeUnit;
}
// 校验必填字段:课程名称, 课程线下
this.handleValidate(commonParams).then((res) => {
if (!res) return;
Upload.uploadTextToOSS(introduce, `${randomString()}.txt`, (introduceId) => {
this.submitRemote({
courseId,
pageType,
commonParams,
introduceId,
});
}, () => message.warning('上传课程简介失败'));
});
}
submitRemote = (data) => {
const { courseId, pageType, commonParams, introduceId } = data;
commonParams.introduceId = introduceId;
if (pageType === 'add') {
Service.Hades('public/hades/createOfflineCourse', commonParams).then((res) => {
if (!res) return;
message.success("新建成功");
window.RCHistory.push({
pathname: `/offline-course`,
});
})
} else {
const editParams = {
courseId:courseId,
...commonParams,
}
Service.Hades('public/hades/updateOfflineCourse', editParams).then((res) => {
if (!res) return;
message.success("保存成功");
window.RCHistory.push({
pathname: `/offline-course`,
});
});
}
}
handleValidate = (data) => {
return new Promise((resolve) => {
if (!data.courseName) {
message.warning('请输入课程名称');
resolve(false);
} else if(!data.categoryId){
message.warning('请选择课程分类');
resolve(false);
} else if(!data.offlinePlace){
message.warning('请输入上课地点');
resolve(false);
} else if(!data.teacherId ){
message.warning('请选择讲师');
resolve(false);
} else if(_.isEmpty(data.calendarTime)){
message.warning('请选择上课日期');
resolve(false);
} else if(!data.startTime || !data.endTime){
message.warning('请选择上课时间');
resolve(false);
} else if(moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.startTime).format(' HH:mm')).valueOf() < Date.now()){
message.warning('上课时间不能早于现在');
resolve(false);
} else if(data.startTime >= data.endTime){
message.warning('上课结束时间不能早于上课开始时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && !data.startTimeApply){
message.warning('请选择报名时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && data.startTimeApply >= data.endTimeApply){
message.warning('报名结束时间需大于报名开始时间');
resolve(false);
} else if(data.whetherSetApply === 'YES' && data.endTimeApply > moment(moment(data.calendarTime[0]).format('YYYY-MM-DD') + moment(data.endTime).format(' HH:mm:ss')).valueOf()){
message.warning('报名结束时间需小于上课开始时间');
resolve(false);
} else if(data.whetherSetSignIn === 'YES' && !data.signInTimeNum){
message.warning('请输入签到时间');
resolve(false);
} else if(data.whetherSetSignOut === 'YES' && ((data.signOutType === 'START_LATER' && !data.signOutStartTimeNum) || !data.signOutEndTimeNum)){
message.warning('请输入签退时间');
resolve(false);
} else if (data.isMore) {
message.warning('简介超过字数限定');
resolve(false);
} else {
resolve(true);
}
});
}
// 使用默认封面图
handleResetCoverUrl = () => {
const { coverUrl } = this.state;
const isDefaultCover = coverUrl === defaultCoverUrl;
// 如果已经是默认图的话,不做任何任何处理
if (isDefaultCover) return;
message.success('已替换为默认图');
this.setState({ coverUrl: defaultCoverUrl });
}
// 滑动加载更多讲师列表
handleScrollTeacherList = (e) => {
const { hasNext } = this.state;
const container = e.target;
//判定元素是否滚动到底部
const scrollToBottom = container && container.scrollHeight <= container.clientHeight + container.scrollTop;
if (scrollToBottom && hasNext) {
const { teacherQuery } = this.state;
let _teacherQuery = teacherQuery;
_teacherQuery.current = _teacherQuery.current + 1
this.setState({
teacherQuery:{..._teacherQuery}
},()=>{this.getTeacherList(_teacherQuery.current)})
}
}
getTeacherList(current = 1, selectList){
const { teacherQuery,teacherList} = this.state;
const _query = {
...teacherQuery,
current,
size:15
};
StoreService.getStoreUserBasicPage( _query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? teacherList.concat(records) : records;
this.setState({
hasNext,
teacherList: list,
teacherQuery:{..._query}
})
});
}
changeIntro = (value, textLength) => {
const isMore = textLength > 1000;
if (isMore) {
message.warning('内容过长,不能超过1000字');
}
this.setState({ introduce: value, isMore });
}
selectMultiDate = (calendarTime) => {
const dateList = _.sortBy(calendarTime);
this.setState({
calendarTime: dateList,
})
}
handleChangeDates = (dates) => {
const data = {};
if (_.isEmpty(dates)) {
data.startTimeApply = undefined;
data.endTimeApply = undefined;
} else {
data.startTimeApply = dates[0].valueOf();
data.endTimeApply = dates[1].startOf('minute').valueOf() + 59000;
}
this.setState(data);
}
whetherVisitorsJoinChange = ()=>{
if(this.state.whetherVisitorsJoin === "NO"){
this.setState({ whetherVisitorsJoin: 'YES' });
}else{
this.setState({ whetherVisitorsJoin: 'NO' });
}
}
handleChangeCatalogList = (value, label) => {
this.setState({ categoryId: value, categoryName: label[0] });
};
render() {
const {
courseId,
pageType,
courseName,
coverUrl,
introduce,
categoryId,
categoryList,
courseCatalogList,
whetherVisitorsJoin,
loadintroduce,
showSelectCoverModal,
visible,
hasImgReady,
cutImageBlob,
teacherId,
teacherList,
calendarTime,
startTime,
endTime,
offlineCourseType,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
startTimeApply,
endTimeApply,
signInTimeUnit,
signInTimeNum,
signOutEndTimeNum,
signOutEndTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signInType,
signOutType,
oldQuta,
quota,
offlinePlace,
isEditDisablie,
} = this.state;
const isDefaultCover = coverUrl === defaultCoverUrl;
return (
<div className="page add-offline-course-page">
<Breadcrumbs
navList={pageType === "add" ? "新建线下课" : "编辑线下课"}
goBack={this.handleGoBack}
/>
<div className="box">
<div className="show-tips">
<ShowTips message="请遵守国家相关规定,切勿上传低俗色情、暴力恐怖、谣言诈骗、侵权盗版等相关内容,小麦企学院保有依据国家规定及平台规则进行处理的权利" />
</div>
<div className="form">
<div className="basic-info__wrap">
<div className="title">基本信息</div>
<div className="course-name">
<span className="label"><span className="require">*</span>课程名称:</span>
<Input
value={courseName}
placeholder="请输入线下课的名称(40字以内)"
maxLength={40}
style={{ width: 240 }}
onChange={(e) => { this.setState({ courseName: e.target.value }) }}
/>
</div>
<div className="course-cover">
<span className="label">封面图:</span>
<div className="course-cover__wrap">
<div className="img-content">
{
isDefaultCover && <span className="tag">默认图</span>
}
<img src={coverUrl} />
</div>
<div className="opt-btns">
<Button onClick={() => {
this.setState({
showSelectCoverModal: true
})
}}>上传图片</Button>
<span
className={`default-btn ${isDefaultCover ? 'disabled' : ''}`}
onClick={this.handleResetCoverUrl}
>使用默认图</span>
<div className="tips">建议尺寸1280*720px,图片支持jpg、jpeg、png格式。</div>
</div>
</div>
</div>
<div className="course-catalog">
<span className="label special"><span className="require">*</span>课程分类:</span>
<TreeSelect
showSearch
treeNodeFilterProp="title"
style={{ width: 240 }}
dropdownStyle={{ maxHeight: 300, overflow: "auto" }}
treeData={courseCatalogList}
placeholder="请选择课程类型"
allowClear
value={categoryId}
treeDefaultExpandAll
onChange={(value, label) => {
this.handleChangeCatalogList(value, label);
}}
/>
</div>
<div className="course-catalog">
<span className="label special"><span className="require">*</span>上课地点:</span>
<Input
value={offlinePlace}
maxLength={40}
style={{ width: 240 }}
placeholder="请输入上课地点(40字以内)"
onChange={(e) => {
this.setState({ offlinePlace: e.target.value })
}}
/>
</div>
<div className="course-catalog" id="teacher">
<span className="label special"><span className="require">* </span>讲师:</span>
<Select
placeholder="请选择讲师"
value={teacherId}
style={{ width: 240 }}
showSearch
allowClear
filterOption={(input, option) => option}
dropdownClassName="offline-dropdown-box"
onPopupScroll={this.handleScrollTeacherList}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
onChange={(value, option) => {
if (option) {
this.setState({ teacherId: value, teacherName: option.children });
}else{
this.setState({ teacherId: value, teacherName: "" });
}
}}
onSearch={(value) => {
let _teacherQuery = {...this.state.teacherQuery};
_teacherQuery.nickName = value
this.setState({
teacherQuery: _teacherQuery
}, () => {
this.getTeacherList()
})
}}
onClear ={(value)=>{
this.setState({
teacherQuery:{
size: 15,
current: 1,
nickName:null
}
}, () => {
this.getTeacherList()
})
}
}
getPopupContainer={() =>
document.getElementById("teacher")
}
>
{_.map(teacherList, (item, index) => {
return (
<Option value={item.id} key={item.id}>{item.nickName}</Option>
);
})}
</Select>
</div>
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch
checked={whetherVisitorsJoin === "YES" ? true : false}
onChange={this.whetherVisitorsJoinChange}
/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看线下课</div>
</div>
</div>
</div>
</div>
<div className="introduce">
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!courseId || loadintroduce) &&
<GraphicsEditor
id="intro"
isIntro={true}
maxLimit={1000}
detail={{
content: introduce
}}
onChange={(val, textLength) => {
this.changeIntro(val, textLength)
}}
/>
}
</div>
</div>
</div>
</div>
<div className="title" style={{ marginTop: 24 }}>课程设置</div>
<div className="day">
<span className="label">
<span className="require">*</span>
上课日期:
</span>
<div>
<div className='select-day'>
已选 <span className="mark-day">{isLongArr(calendarTime) ? calendarTime.length : 0}</span>
</div>
<MultipleDatePicker
disabled={isEditDisablie}
selectDateList={calendarTime}
onSelect={this.selectMultiDate}
canSelectTodayBefore={false}
/>
</div>
</div>
<div className="hour" id="hour">
<span className="label"><span className="require">*</span>上课时间:</span>
<TimePicker
disabled={isEditDisablie}
className="time-picker"
format="HH:mm"
value={startTime ? moment(startTime) : null}
placeholder="开始时间"
showNow={false}
style={{ width: 100, minWidth: 100}}
onSelect={(time) => {
this.setState({ startTime: time });
}}
getPopupContainer={() =>
document.getElementById("hour")
}
/>&nbsp;&nbsp;~&nbsp;&nbsp;
<TimePicker
disabled={isEditDisablie}
className="time-picker"
format="HH:mm"
value={endTime ? moment(endTime) : null}
placeholder="结束时间"
showNow={false}
style={{ width: 100, minWidth: 100 }}
onSelect={(time) => {
this.setState({ endTime: time });
}}
getPopupContainer={() =>
document.getElementById("hour")
}
/>
</div>
<div className="course-catalog">
<span className="label"><span className="require">* </span>学员上课方式:</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={offlineCourseType}
onChange={(e) => {
this.setState({ offlineCourseType: e.target.value });
}}
className="mt5"
disabled={isEditDisablie}
>
<Radio value="ALL_DAY_OFFLINE" className="mr-16">
<span style={{ color: "#333" }}>所选日期都要上课</span>
</Radio>
<Radio value="ANY_DAY_POFFLINE" className="mr-16">
<span style={{ color: "#333" }}>选择任意1天上课</span>
</Radio>
</Radio.Group>
</div>
<div className="course-catalog">
<span className="label">课程报名:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<Switch
disabled={isEditDisablie}
checked={whetherSetApply === 'YES'}
onChange={(value) => {
this.setState({
whetherSetApply: value ? 'YES' : 'NO',
startTimeApply: undefined,
endTimeApply: undefined,
quota: null,
});
}}
/>
<span className="switch-tip">开启后可设置课程报名时间,获取报名数据</span>
</div>
{whetherSetApply === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">报名日期:</span>
<RangePicker
id="course_date_picker"
showTime={{ showTime: 'HH:mm' }}
allowClear={false}
value={startTimeApply ? [moment(startTimeApply), moment(endTimeApply)] : null }
format={"YYYY-MM-DD HH:mm"}
onChange={(dates) => { this.handleChangeDates(dates) }}
renderExtraFooter={() => calendarTime[0] ? <div style={{ position: 'absolute', bottom: 8, cursor: 'pointer' }}>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(1, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
style={{
color: '#FFB714',
border: '1px solid #FFB714',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前1天</span>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(2, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
style={{
color: '#FFB714',
border: '1px solid #FFB714',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前2天</span>
<span
onClick={() => this.setState({ startTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).subtract(3, 'days').valueOf(), endTimeApply: moment(`${moment(calendarTime[0]).format('YYYY-MM-DD')} ${moment(startTime).format('HH:mm')}`).valueOf() - 1000 })}
style={{
color: '#FFB714',
border: '1px solid #FFB714',
padding: '2px 8px',
borderRadius: '2px',
marginRight: 8,
}}
>上课前3天</span>
</div> : null}
/>
</div>}
{whetherSetApply === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">
报名人数
<Tooltip title="报名一旦开始,报名人数不支持减少">
<span style={{ margin: '0 4px', color: '#999' }} className="icon iconfont">&#xe7c4;</span>
</Tooltip>
:最多
</span>
<InputNumber
value={quota}
min={oldQuta || 1}
max={100000}
precision={0}
style={{ margin: '0 4px', width: 90 }}
disabled={oldQuta < 0}
onChange={(value) => {
this.setState({ quota: value })
}}
/>
<span className="switch-label"></span>
<span className="switch-tip">未填写时默认为不限制</span>
</div>}
</div>
</div>
<div className="course-catalog">
<span className="label">考勤签到:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<Switch
checked={whetherSetSignIn === 'YES'}
onChange={(value) => {
this.setState({
whetherSetSignIn: value ? 'YES' : 'NO',
signInType: 'START_AGO',
signInTimeNum: null,
signInTimeUnit: 'MINUTE',
})
}}
/>
<span className="switch-tip">开启后可设置获取签到考勤数据</span>
</div>
{whetherSetSignIn === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">签到时间:</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={signInType}
onChange={(e) => {
this.setState({ signInType: e.target.value });
}}
className="mt5"
>
<Radio value="START_AGO" className="mr-16">
<span style={{ color: "#333" }}>课程开始前</span>
</Radio>
<Radio value="END_AGO" className="mr-16">
<span style={{ color: "#333" }}>课程结束前</span>
</Radio>
</Radio.Group>
</div>}
{whetherSetSignIn === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">课程{signInType === 'START_AGO' ? '开始' : '结束'}</span>
<InputNumber
value={signInTimeNum}
min={1}
max={signInTimeUnit === 'MINUTE' ? 1440 : 24}
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signInTimeNum: value })
}}
/>
<Select
style={{ width: 72, marginRight: 4 }}
value={signInTimeUnit}
onChange={(value) => {
const data = { signInTimeUnit: value }
if (value === 'HOUR' && signInTimeNum > 24) {
data.signInTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
))}
</Select>
<span className="switch-label">内可签到</span>
</div>}
</div>
</div>
<div className="course-catalog">
<span className="label">考勤签退:</span>
<div className="switch-box">
<div className="switch-item" key="1">
<Switch
checked={whetherSetSignOut === 'YES'}
onChange={(value) => {
this.setState({
whetherSetSignOut: value ? 'YES' : 'NO',
signOutType: 'START_LATER',
signOutStartTimeNum: null,
signOutStartTimeUnit: 'MINUTE',
signOutEndTimeNum: null,
signOutEndTimeUnit: 'MINUTE',
})
}}
/>
<span className="switch-tip">开启后可设置获取签退考勤数据</span>
</div>
{whetherSetSignOut === 'YES' && <div className="switch-item" key="2">
<span className="switch-label">签退时间:</span>
<Radio.Group
style={{ display: 'inline-block' }}
value={signOutType}
onChange={(e) => {
this.setState({ signOutType: e.target.value });
}}
className="mt5"
>
<Radio value="START_LATER" className="mr-16">
<span style={{ color: "#333" }}>课程开始后</span>
</Radio>
<Radio value="END_LATER" className="mr-16">
<span style={{ color: "#333" }}>课程结束后</span>
</Radio>
</Radio.Group>
</div>}
{whetherSetSignOut === 'YES' && <div className="switch-item" key="3">
<span className="switch-label">课程{signOutType === 'START_LATER' ? '开始' : '结束'}</span>
{signOutType === 'START_LATER' && <InputNumber
value={signOutStartTimeNum}
min={1}
max={signOutStartTimeUnit === 'MINUTE' ? 1440 : 24}
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signOutStartTimeNum: value })
}}
/>}
{signOutType === 'START_LATER' && <Select
style={{ width: 72, marginRight: 4 }}
value={signOutStartTimeUnit}
onChange={(value) => {
const data = { signOutStartTimeUnit: value }
if (value === 'HOUR' && signOutStartTimeNum > 24) {
data.signOutStartTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
))}
</Select>}
{signOutType === 'START_LATER' && <span className="switch-label">就可签退,截止签退时间为下课后</span>}
<InputNumber
value={signOutEndTimeNum}
min={1}
max={signOutEndTimeUnit === 'MINUTE' ? 1440 : 24}
precision={0}
style={{ margin: '0 4px', width: 90 }}
onChange={(value) => {
this.setState({ signOutEndTimeNum: value })
}}
/>
<Select
style={{ width: 72, marginRight: 4 }}
value={signOutEndTimeUnit}
onChange={(value) => {
const data = { signOutEndTimeUnit: value }
if (value === 'HOUR' && signOutEndTimeNum > 24) {
data.signOutEndTimeNum = 24;
}
this.setState(data);
}}
>
{unitList.map(item => (
<Option value={item.key} key={item.key}>{item.value}</Option>
))}
</Select>
{signOutType !== 'START_LATER' &&
<span className="switch-label">内可签退</span>
}
</div>}
</div>
</div>
</div>
</div>
</div>
<div className="footer">
<Button onClick={this.handleGoBack}>取消</Button>
<Button onClick={this.handleShowPreviewModal}>预览</Button>
<Button type="primary" onClick={_.debounce(() => this.preSubmit(), 3000, true)}>保存</Button>
</div>
{showSelectCoverModal &&
<SelectPrepareFileModal
key="basic"
operateType="select"
multiple={false}
accept="image/jpeg,image/png,image/jpg"
selectTypeList={['JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png'
isOpen={showSelectCoverModal}
onClose={() => {
this.setState({ showSelectCoverModal: false })
}}
onSelect={this.handleSelectCover}
/>
}
<Modal
title="设置图片"
width={1080}
visible={visible}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
onCancel={() => {
this.setState({ visible: false });
}}
zIndex={10001}
footer={[
<Button
key="back"
onClick={() => {
this.setState({ visible: false });
}}
>
重新上传
</Button>,
<Button
key="submit"
type="primary"
disabled={!hasImgReady}
onClick={() => {
if (!cutFlag) {
cutFlag = true;
this.refs.hiddenBtn.click();
}
this.getSignature(cutImageBlob);
}}
>
确定
</Button>,
]}
>
<div className="clip-box">
<div
id="headPicModal"
ref="headPicModal"
style={{
width: "500px",
height: "430px",
marginBottom: 0,
}}
></div>
<div id="clipBtn" style={{ display: "none" }} ref="hiddenBtn"></div>
<div className="preview-img">
<div className="title">效果预览</div>
<div id="preview-url-box" style={{width:500,height:282}}>
<img src={this.state.dataUrl} style={{ width: '100%' }} alt="" />
</div>
<div className="tip-box">
<div className="tip">温馨提示</div>
<div className="tip">①预览效果图时可能存在延迟,单击左侧图片刷新即可</div>
<div className="tip">②设置图片时双击可旋转图片,滚动可放大或缩小图片</div>
</div>
</div>
</div>
</Modal>
{ this.state.previewOfflineModal }
</div>
)
}
}
export default AddOfflineCourse;
.add-offline-course-page {
position:relative !important;
.box{
margin-bottom:66px !important;
}
.ant-radio-group {
display: flex;
flex-direction: column;
margin-top: 0 !important;
.radio-item {
margin-bottom: 12px;
.text {
color: #333;
}
.sub-text {
color: #999;
}
}
.ant-radio {
vertical-align: top;
padding-top: 2px;
}
}
.form {
margin-top: 16px;
padding: 0 16px;
.basic-info__wrap {
.title {
font-size: 16px;
color: #333;
font-weight: 500;
line-height: 22px;
margin-bottom: 16px;
margin-left: -16px;
}
}
.label{
display: inline-block;
text-align: right;
width: 120px;
flex-shrink: 0;
&.special {
line-height: 32px;
}
}
.required {
position: relative;
&::before {
position: absolute;
content: '*';
color: red;
left: 5px;
top: 6px;
}
&.label::before {
top: 0;
}
}
.course-catalog {
margin-bottom:16px;
margin-top:16px;
display: flex;
.switch-box {
.switch-item {
display: flex;
align-items: center;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.switch-label {
color: #666;
}
.switch-tip {
color: #999;
margin-left: 4px;
}
}
}
}
.require {
color: red;
}
.introduce {
display: flex;
flex-direction: row;
}
.day {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.select-day {
margin-bottom: 4px;
.mark-day {
color: #FFB714;
}
}
}
.allow-tourist-join{
display:flex;
margin-bottom:16px;
.desc {
color:#999;
margin-left:12px;
}
.content{
display:flex;
}
}
.course-ware {
display: flex;
align-items: center;
margin-bottom: 4px;
&__img {
width: 24px;
margin-right: 4px;
}
&__name {
color: #333;
}
}
.flex {
display: flex;
}
.course-cover {
display: flex;
margin-top: 16px;
&__wrap {
position: relative;
.tag {
border-radius: 2px;
background: #D6D6D6;
font-size: 12px;
height: 18px;
width: 52px;
text-align: center;
color: #FFF;
position: absolute;
top: 8px;
left: 8px;
}
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
margin-right: 20px;
width: 299px;
height: 169px;
img {
width: 100%;
height: 100%;
object-fit: contain;
border: 1px solid #E8e8e8;
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #5289FA;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
.ant-upload-list {
display: none;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
.select-student {
align-items: center;
margin-left: 24px;
margin-top: 8px;
.has-selected {
margin-left: 12px;
color: #333;
}
}
.sub-content {
margin-left: 85px;
margin-top: 4px;
.tips {
margin-left: 4px;
color: #999;
}
}
}
.footer {
position: fixed;
bottom: 0;
height: 58px;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 252px;
background: #fff;
border-top: 1px solid #E8E8E8;
z-index: 999;
.ant-btn {
margin-left: 10px;
}
}
}
.offline-dropdown-box {
.rc-virtual-list-scrollbar {
display: none;
position: absolute;
left: 20000px;
}
}
\ No newline at end of file
import React from 'react';
import { Tooltip, Input, Radio, Table, Checkbox } from 'antd';
import moment from 'moment';
import _ from 'underscore';
import Breadcrumbs from "@/components/Breadcrumbs";
import PageControl from '@/components/PageControl';
import Service from "@/common/js/service";
import User from '@/common/js/user';
import './OfflineCourseData.less';
const { Search } = Input;
export default class OfflineCourseData extends React.Component {
constructor(props) {
super(props);
const courseId = window.getParameterByName('id');
this.state = {
courseId,
query: {
size: 10,
current: 1,
courseId,
storeId: User.getStoreId(),
},
loading: false,
data: [],
total: 0,
courseName: '',
calendarTime: [],
currentIndex: 0,
fullJoin: 0,
totalJoin: 0,
fullJoinNum: 0,
joinInNum: 0,
joinNum: 0,
joinOutNum: 0,
}
}
componentDidMount() {
this.getOfflineBasic();
}
getOfflineBasic = () => {
const { courseId, currentIndex } = this.state;
Service.Hades('public/hades/getOfflineCourseJoinBase', { courseId }).then((res) => {
if (res.success) {
const { result } = res;
const group = _.groupBy(result.calendarTime, item => moment(item).format('YYYY-MM'));
const calendarTime = _.map(group, (value, key) => ({ key, value }));
const currentDate = calendarTime[currentIndex].value[0];
this.setState({
courseName: result.courseName,
fullJoin: result.fullJoin || 0,
totalJoin: result.totalJoin || 0,
calendarTime,
currentDate,
offlineCourseType: result.offlineCourseType,
whetherSetSignIn: result.whetherSetSignIn,
whetherSetSignOut: result.whetherSetSignOut,
whetherSetSignInOut: result.whetherSetSignInOut,
whetherSetApply: result.whetherSetApply,
}, () => this.getDateDetail());
}
})
}
getDateDetail = (current = 1) => {
const { query, currentDate, courseId } = this.state;
if (currentDate !== query.date) {
Service.Hades('public/hades/getOfflineDateJoinStatistics', { courseId, date: currentDate }).then((res) => {
if (res.success) {
this.setState({
fullJoinNum: res.result.fullJoinNum || 0,
joinInNum: res.result.joinInNum || 0,
joinNum: res.result.joinNum || 0,
joinOutNum: res.result.joinOutNum || 0,
})
}
})
}
query.current = current;
query.date = currentDate;
Service.Hades('public/hades/getOfflineDateJoinPage', query).then((res) => {
if (res.success) {
this.setState({ data: res.result.records, total: res.result.total });
}
})
}
getColumns = () => {
const { whetherSetSignIn, whetherSetSignOut, whetherSetApply } = this.state;
const columns = [
{
title: '学员姓名',
key: 'name',
dataIndex: 'name',
render: (val, record) => {
return (
<div>{record.name}</div>
)
}
},
{
title: "手机号",
key: "phone",
dataIndex: "phone",
render: (val, item) => {
return (
<div>{item.phone || '-'}</div>
)
},
},
];
whetherSetApply === 'YES' && columns.push({
title: '报名时间',
key: 'joinTime',
dataIndex: 'joinTime',
render: (val, item) => {
return <div>{formatDate('YYYY-MM-DD H:i', item.joinTime)}</div>
}
});
whetherSetSignIn === 'YES' && columns.push({
title: '签到时间',
key: 'joinInTime',
dataIndex: 'joinInTime',
sorter: true,
render: (val) => {
return val ? formatDate('YYYY-MM-DD H:i', val) : '-';
}
})
whetherSetSignOut === 'YES' && columns.push({
title: '签退时间',
key: 'joinOutTime',
dataIndex: 'joinOutTime',
sorter: true,
render: (val) => {
return val ? formatDate('YYYY-MM-DD H:i', val) : '-';
}
})
return columns;
}
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
const { query } = this.state;
let orderEnum;
// 按创建时间升序排序
if (columnKey === 'joinInTime' && order === 'ascend') {
orderEnum = 'JOIN_IN_ASC';
} else if (columnKey === 'joinOutTime' && order === 'ascend') {
orderEnum = 'JOIN_OUT_ASC';
} else if (columnKey === 'joinInTime' && order === 'descend') {
orderEnum = 'JOIN_IN_DESC';
} else if (columnKey === 'joinOutTime' && order === 'descend') {
orderEnum = 'JOIN_OUT_DESC';
}
const _query = {
...query,
orderEnum,
};
this.setState({ query: _query }, () => {
this.getDateDetail(_query.current);
})
}
render() {
const {
query,
loading,
data,
total,
courseName,
fullJoin,
totalJoin,
calendarTime,
currentIndex,
currentDate,
fullJoinNum,
joinInNum,
joinNum,
joinOutNum,
offlineCourseType,
whetherSetApply,
whetherSetSignIn,
whetherSetSignOut,
whetherSetSignInOut,
} = this.state;
const calendarLength = calendarTime.length;
const dateList = (calendarTime[currentIndex] || {}).value || [];
return (
<div className="page offline-course-data">
<Breadcrumbs
navList="参与数据"
goBack={() => {
window.RCHistory.goBack();
}}
/>
<div className="box">
<div className="offline-name">课程名称:{courseName}</div>
{whetherSetApply === 'YES' && <div className="offline-application">报名总人数:{totalJoin}</div>}
{whetherSetSignInOut === 'YES' && <div className="offline-application">
完成考勤总人数<Tooltip title="根据上课日期,在规定时间内完成签到和签退的学员数"><span className="icon iconfont">&#xe7c4;</span></Tooltip>{fullJoin}
</div>}
</div>
<div className="box data-box">
<div className="left-box">
<div className="left-title">上课日期</div>
<div className="left-calendar">
<div
className="icon-box"
onClick={() => {
const index = currentIndex - 1;
if (index >= 0 && index < calendarLength) {
this.setState({ currentIndex: index, currentDate: calendarTime[index].value[0] }, () => {
this.getDateDetail();
});
}
}}
>
<span className="icon iconfont">&#xe79c;</span>
</div>
<div className="calendar-text">{(calendarTime[currentIndex] || {}).key}</div>
<div
className="icon-box"
onClick={() => {
const index = currentIndex + 1;
if (index >= 0 && index < calendarLength) {
this.setState({ currentIndex: index, currentDate: calendarTime[index].value[0] }, () => {
this.getDateDetail();
});
}
}}
>
<span className="icon iconfont">&#xe79b;</span>
</div>
</div>
<div className="date-list">
{dateList.map(item => (
<div
className={`date-item${item === currentDate ? ' selected' : ''}`}
key={item}
onClick={() => {
this.setState({ currentDate: item }, () => {
this.getDateDetail();
})
}}
>{window.formatDate('MM月DD日(WW)', item)}</div>
))}
</div>
</div>
<div className="right-box">
<div className="selected-date">{moment(currentDate).format('MM-DD')}</div>
<div className="detail-data">
{offlineCourseType !== 'ALL_DAY_OFFLINE' && whetherSetApply === 'YES' && <span className="icon iconfont">&#xe89f;</span>}
{offlineCourseType !== 'ALL_DAY_OFFLINE' && whetherSetApply === 'YES' && <span className="data-text">报名人数:{joinNum}</span>}
{whetherSetSignInOut === 'YES' && <span className="icon iconfont">&#xe89e;</span>}
{whetherSetSignInOut === 'YES' && <span className="data-text">完成考勤数<Tooltip title="当日在规定时间内完成签到和签退的学员数"><span className="icon iconfont">&#xe7c4;</span></Tooltip>{fullJoinNum}</span>}
{whetherSetSignIn === 'YES' && <span className="icon iconfont">&#xe8a0;</span>}
{whetherSetSignIn === 'YES' && <span className="data-text">签到人数:{joinInNum}</span>}
{whetherSetSignOut === 'YES' && <span className="icon iconfont">&#xe89d;</span>}
{whetherSetSignOut === 'YES' && <span className="data-text">签退人数:{joinOutNum}</span>}
</div>
<div className="detail-filter">
<Search
className="search-input"
placeholder="搜索学员姓名/手机号"
style={{ width: 200, marginRight: 24 }}
enterButton={<span className="icon iconfont">&#xe832;</span>}
onSearch={(value) => {
const _query = { ...query };
if (value) {
const isPhone = (value || "").match(/^\d+$/);
const name = isPhone ? "storeCustomerPhone" : "storeCustomerName";
_query.storeCustomerName = "";
_query.storeCustomerPhone = "";
_query[name] = value;
} else {
_query.storeCustomerName = "";
_query.storeCustomerPhone = "";
}
this.setState({ query: _query }, () => this.getDateDetail());
}}
/>
{whetherSetSignInOut === 'YES' && <div className="filter-box">
<span className="label">签到情况:</span>
<Checkbox
checked={query.joinInState === 'YES'}
onChange={(e) => {
if (e.target.checked) {
this.setState({ query: { ...query, joinInState: 'YES' } }, () => this.getDateDetail());
} else {
delete query.joinInState;
this.setState({ query }, () => this.getDateDetail())
}
}}
>已签到</Checkbox>
<Checkbox
checked={query.joinInState === 'NO'}
onChange={(e) => {
if (e.target.checked) {
this.setState({ query: { ...query, joinInState: 'NO' } }, () => this.getDateDetail());
} else {
delete query.joinInState;
this.setState({ query }, () => this.getDateDetail())
}
}}
>未签到</Checkbox>
</div>}
{whetherSetSignOut === 'YES' && <div className="filter-box">
<span className="label">签退情况:</span>
<Checkbox
checked={query.joinOutState === 'YES'}
onChange={(e) => {
if (e.target.checked) {
this.setState({ query: { ...query, joinOutState: 'YES' } }, () => this.getDateDetail());
} else {
delete query.joinOutState;
this.setState({ query }, () => this.getDateDetail())
}
}}
>已签退</Checkbox>
<Checkbox
checked={query.joinOutState === 'NO'}
onChange={(e) => {
if (e.target.checked) {
this.setState({ query: { ...query, joinOutState: 'NO' } }, () => this.getDateDetail());
} else {
delete query.joinOutState;
this.setState({ query }, () => this.getDateDetail())
}
}}
>未签退</Checkbox>
</div>}
</div>
<Table
bordered
size="middle"
pagination={false}
columns={this.getColumns()}
loading={loading}
dataSource={data}
onChange={this.handleChangeTable}
rowKey={(row) => row.userId}
/>
<PageControl
current={query.current - 1}
pageSize={query.size}
total={total}
toPage={(page) => {
this.getDateDetail(page + 1);
}}
/>
</div>
</div>
</div>
)
}
}
\ No newline at end of file
.offline-course-data {
min-width: 1100px;
.box {
.offline-name {
font-size: 19px;
color: #333;
line-height: 26px;
font-weight: 500;
padding-left: 8px;
position: relative;
&::after {
position: absolute;
width: 4px;
height: 16px;
content: '';
background: #FFB714;
left: 0;
top: 5px;
}
}
.offline-application {
color: #333;
font-size: 14px;
line-height: 20px;
margin-top: 8px;
.iconfont {
color: #BFBFBF;
font-size: 14px;
margin: 0 4px;
}
}
}
.data-box {
padding: 16px 0 !important;
display: flex;
.left-box {
width: 260px;
border-right: 1px solid #EEEEEE;
.left-title {
color: #000;
font-size: 16px;
font-weight: 500;
line-height: 22px;
padding-left: 8px;
padding-bottom: 16px;
margin: 0 8px;
border-bottom: 1px solid #E8E8E8;
}
.left-calendar {
margin-top: 16px;
margin-left: 20px;
display: flex;
align-items: center;
.icon-box {
width: 32px;
height: 32px;
border-radius: 4px;
border: 1px solid #E8E8E8;
display: flex;
align-items: center;
justify-content: center;
color: #BFBFBF;
cursor: pointer;
.iconfont {
font-size: 12px;
}
}
.calendar-text {
color: #FF9D14;
line-height: 20px;
margin: 0 44px;
}
}
.date-list {
width: 100%;
margin-top: 16px;
.date-item {
height: 44px;
width: 100%;
display: flex;
padding: 12px 32px;
color: #666;
&:hover {
background: #F3F6FA;
cursor: pointer;
}
&.selected {
background: rgba(255, 183, 20, 0.06);
}
}
}
}
.right-box {
padding: 0 16px;
width: ~'calc(100% - 261px)';
.selected-date {
font-size: 16px;
color: #333;
line-height: 22px;
font-weight: 500;
}
.detail-data {
display: flex;
margin-top: 10px;
align-items: center;
.iconfont {
font-size: 15px;
color: #999999;
margin-right: 4px;
}
.data-text {
color: #333;
font-size: 14px;
line-height: 20px;
margin-right: 24px;
.iconfont {
font-size: 14px;
color: #BFBFBF;
margin: 0 4px;
}
}
}
.detail-filter {
display: flex;
margin-top: 16px;
margin-bottom: 16px;
align-items: center;
.filter-box {
margin-right: 36px;
}
}
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-16 11:05:17
* @Last Modified by: chenshu
* @Last Modified time: 2021-04-06 16:17:57
* @Description: 添加直播-简介
*/
import React from 'react';
import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd';
import Service from '@/common/js/service';
import GraphicsEditor from '../../components/GraphicsEditor';
import User from '@/common/js/user';
import UploadOss from '@/core/upload';
import './AddGraphicsIntro.less';
import SelectPrepareFileModal from '@/modules/prepare-lesson/modal/SelectPrepareFileModal';
import { DISK_MAP } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
const { TextArea } = Input;
class AddGraphicsIntro extends React.Component {
constructor(props) {
super(props);
this.state = {
showSelectFileModal: false,
diskList: [],
selectType: null,
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 选择暖场资源
handleSelectVideo = (file) => {
const { selectType } = this.state;
this.setState({
showSelectFileModal: false
})
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
if(selectType === 'WARMUP'){
const liveCourseWarmMedia = {
contentType: 'WARMUP',
mediaType: folderFormat === 'MP4' ? 'VIDEO' : 'PICTURE',
mediaContent: resourceId,
mediaUrl: ossUrl,
mediaName: folderName,
size: folderSize
}
this.props.onChange('liveCourseWarmMedia', liveCourseWarmMedia);
}else{
// 最多添加九图片
const { liveCourseMediaRequests } = this.props.data;
const list = _.filter(liveCourseMediaRequests, (item) => {
return item.mediaType == "PICTURE";
});
if (list.length > 8) {
message.warning("最多添加9张图片");
return;
}
liveCourseMediaRequests.push({
contentType: 'INTRO',
size: folderSize,
mediaName: folderName,
mediaContent: resourceId,
mediaType: 'PICTURE',
mediaUrl: ossUrl,
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
}
changeDetail = (value) => {
this.props.onChange('courseMedia', value);
}
changeIntro = (value) => {
this.props.onChange('introduce', value);
}
whetherVisitorsJoinChange = ()=>{
if(this.props.data.whetherVisitorsJoin==="NO"){
this.props.onChange('whetherVisitorsJoin','YES')
}else{
this.props.onChange('whetherVisitorsJoin','NO')
}
}
shelfStateChange = ()=>{
if(this.props.data.shelfState==="NO"){
this.props.onChange('shelfState','YES')
}else{
this.props.onChange('shelfState','NO')
}
}
render() {
const {data: { id, whetherVisitorsJoin, courseMedia, introduce, shelfState, loadcourseMedia, loadintroduce } } = this.props;
const { showSelectFileModal, selectType } = this.state;
return (
<div className="add-video__intro-info">
<div className="allow-tourist-join">
<span className="label">观看设置:</span>
<div className="content">
<div>
<Switch checked={whetherVisitorsJoin==="YES"? true:false} onChange={this.whetherVisitorsJoinChange}/>
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看图文课</div>
</div>
</div>
</div>
</div>
<div className="store-show">
<span className="label">学院展示:</span>
<div className="content">
<Row>
<Col span={3}>
<Switch checked={shelfState==="YES"? true:false} onChange={this.shelfStateChange}/>
</Col>
<Col span={21}>
<div className="desc">
<div>开启:图文课将在学员学院图文课列表中展示</div>
<div>关闭:图文课将在学员学院图文课列表中隐藏</div>
</div>
</Col>
</Row>
</div>
</div>
<div className="introduce required">
<span className="label" style={{ marginTop: 5 }}>课程内容:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item content-editor">
{(!id || loadcourseMedia) &&
<GraphicsEditor
id="content"
detail={{
content: courseMedia
}}
onChange={(val) => { this.changeDetail(val) }}
/>
}
</div>
</div>
</div>
</div>
<div className="introduce">
<span className="label">课程简介:</span>
<div className="content">
<div className="intro-list">
<div className="intro-list__item introduce-editor">
{(!id || loadintroduce) &&
<GraphicsEditor
id="intro"
isIntro={true}
detail={{
content: introduce
}}
onChange={(val) => { this.changeIntro(val) }}
/>
}
</div>
</div>
</div>
</div>
{/* 选择暖场图文件弹窗 */}
{ showSelectFileModal &&
<SelectPrepareFileModal
operateType="select"
accept={selectType==="INTRO"?"image/jpeg,image/png,image/jpg":"video/mp4,image/jpeg,image/png,image/jpg"}
selectTypeList={ selectType==="INTRO" ? ['JPG', 'JPEG', 'PNG']: ['MP4', 'JPG', 'JPEG', 'PNG'] }
tooltip={ selectType==="INTRO"?'支持文件类型:jpg、jpeg、png':'支持文件类型:jpg、jpeg、png、mp4'}
isOpen={showSelectFileModal}
onClose={() => {
this.setState({ showSelectFileModal: false })
}}
onSelect={this.handleSelectVideo}
/>
}
</div>
)
}
}
export default AddGraphicsIntro;
.add-video__intro-info {
.w-e-full-screen-editor {
background: #fff !important;
}
.playback {
margin-bottom: 10px;
.require {
color: #EC4B35;
}
&__text {
color: #999;
}
}
.label{
display:inline-block;
text-align:right;
width:85px;
}
.playback,
.introduce {
display: flex;
flex-direction: row;
}
.allow-tourist-join{
display:flex;
.content{
display:flex;
}
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
}
}
.store-show{
display:flex;
margin-top:16px;
margin-bottom:16px;
.desc{
margin-left:16px;
font-size:14px;
color:#999;
display:inline-block;
}
}
.radio {
display: block;
height: 30px;
line-height: 30px;
}
.interactive-playback {
display: flex;
margin-bottom: 20px;
}
textarea.ant-input {
min-height: 80px;
}
.intro-list__item {
display: flex;
margin-bottom: 16px;
position: relative;
&.picture {
width: 550px;
padding: 16px;
border: 1px solid #EEE;
border-radius: 4px;
.img__wrap {
width: 299px;
height: 168px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.little-icon {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
right: -20px;
.iconfont {
width: 20px;
height: 20px;
line-height: 20px;
font-size: 20px;
color: #999;
margin-bottom: 4px;
cursor: pointer;
&.close {
margin-top: 8px;
background-image: url('https://image.xiaomaiketang.com/xm/eesMPhNP3e.png');
background-size: 100% 100%;
}
}
}
}
.operate {
display: flex;
align-items: center;
justify-content: center;
width: 550px;
height: 80px;
line-height: 80px;
padding: 16px;
margin-top: 16px;
border: 1px dashed #EBEBEB;
border-radius: 4px;
.ant-upload {
vertical-align: middle;
}
&__item {
display: flex;
flex-direction: column;
cursor: pointer;
&:not(:last-child) {
margin-right: 82px;
}
.iconfont {
font-size: 22px;
line-height: 22px;
color: #BFBFBF;
text-align: center;
}
.text {
color: #999;
line-height: 20px;
margin-top: 4px;
}
}
}
.tips {
color: #999;
margin-top: 16px;
margin-bottom: 8px;
}
.checkExample {
width: 60px;
color: #FF7519;
cursor: pointer;
}
.warmup {
margin-bottom: 17px;
display: flex;
}
.course-cover__wrap {
display: flex;
flex-direction: row;
}
.img-content {
position: relative;
margin-right: 20px;
width: 300px;
height: 170px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.img-delete-wrap {
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
position: absolute;
left: 50%;
top: 50%;
width: 40px;
height: 40px;
transform: translate(-50%, -50%);
}
&:hover {
opacity: 1;
cursor: pointer;
}
}
}
.opt-btns {
.default-btn {
margin-left: 16px;
color: #FF7519;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
}
}
.example-wrap {
font-family: PingFangSC-Regular, PingFang SC;
text-align: center;
.title {
margin-bottom: 6px;
font-size: 14px;
color: #333333;
}
.text {
margin-bottom: 16px;
font-size: 12px;
color: #999999;
}
img {
width: 180px;
}
}
.check-record-rule {
width: 120px;
color: #FF7519;
cursor: pointer;
z-index: 2;
}
.record-rule-wrap {
text-align: left;
ul {
margin-top: 10px;
padding-left: 34px;
list-style-type: disc;
li {
color: #999;
}
}
.text {
color: #999;
}
}
.auto-send-class-report {
.label {
width: 57px;
height: 12px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 12px;
}
.open-text, .close-text {
width: 572px;
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
margin-left: 100px;
margin-top: 5px;
}
.open-text {
margin-top: 8px;
}
.close-text {
margin-bottom: 16px;
}
}
\ No newline at end of file
import React from 'react';
import { Row, Input, Select,Tooltip } from 'antd';
import RangePicker from "@/modules/common/DateRangePicker";
import './OfflineCourseFilter.less';
import moment from 'moment';
import StoreService from "@/domains/store-domain/storeService";
const { Search } = Input;
const { Option } = Select;
const DEFAULT_QUERY = {
courseName: null, // 课程名称
operatorId: null, // 创建人
beginTime: null, // 开始日期
endTime: null, // 结束日期
shelfState:null,
}
const defaultTeacherQuery = {
size: 10,
current: 1,
nickName:null
}
class OfflineCourseFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
query: { ...DEFAULT_QUERY }, // 使用扩展运算符,避免浅拷贝
teacherQuery: defaultTeacherQuery,
teacherList:[],
expandFilter:false
}
}
componentDidMount() {
this.getTeacherList();
}
getTeacherList(current = 1, selectList){
const { teacherQuery,teacherList} = this.state;
const _query = {
...teacherQuery,
current,
size:10
};
StoreService.getStoreUserBasicPage( _query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? teacherList.concat(records) : records;
this.setState({
hasNext,
teacherList: list,
})
});
}
// 滑动加载更多讲师列表
handleScrollTeacherList = (e) => {
const { hasNext } = this.state;
const container = e.target;
const scrollToBottom = container && container.scrollHeight <= container.clientHeight + container.scrollTop;
if (scrollToBottom && hasNext) {
const { teacherQuery } = this.state;
let _teacherQuery = teacherQuery;
_teacherQuery.current = _teacherQuery.current + 1
this.setState({
teacherQuery:{..._teacherQuery}
},()=>{this.getTeacherList(_teacherQuery.current)})
}
}
// 改变搜索条件
handleChangeQuery = (field, value) => {
this.setState({
query: {
...this.state.query,
[field]: value,
current: 1,
}
}, () => {
if (field === 'courseName') return;
this.props.onChange(this.state.query)
});
}
handleChangeDates = (dates) => {
const query = _.clone(this.state.query);
if (_.isEmpty(dates)) {
delete query.beginTime;
delete query.endTime;
} else {
query.beginTime = dates[0].valueOf();
query.endTime = dates[1].valueOf();
}
this.setState({
query:{
...query,
current: 1,
}
}, () => {
this.props.onChange(this.state.query);
})
}
// 重置搜索条件
handleReset = () => {
this.setState({
query: DEFAULT_QUERY,
}, () => {
this.props.onChange(this.state.query);
})
}
render() {
const {
query: {
courseName,
courseState,
shelfState,
},
expandFilter,
} = this.state;
return (
<div className="video-course-filter">
<Row type="flex" justify="space-between" align="top">
<div className="search-condition">
<div className="search-condition__item">
<span className="search-name">线下课名称:</span>
<Search
value={courseName}
placeholder="搜索线下课名称"
onChange={(e) => { this.handleChangeQuery('courseName', e.target.value)}}
onSearch={ () => { this.props.onChange(this.state.query) } }
style={{ width: "calc(100% - 84px)" }}
enterButton={<span className="icon iconfont" style={{fontSize: '16px'}}>&#xe832;</span>}
/>
</div>
<div className="search-condition__item">
<span className="shelf-status">学院展示:</span>
<Select
style={{ width: "calc(100% - 84px)" }}
placeholder="请选择"
allowClear={true}
value={shelfState}
onChange={(value) => { this.handleChangeQuery('shelfState', value) }}
suffixIcon={<span className="icon iconfont" style={{fontSize:'12px',color:'#BFBFBF'}}>&#xe835;</span>}
>
<Option value="YES">开启</Option>
<Option value="NO">关闭</Option>
</Select>
</div>
</div>
<div className="reset-fold-area">
<Tooltip title="清空筛选"><span className="resetBtn iconfont icon" onClick={this.handleReset}>&#xe61b; </span></Tooltip>
</div>
</Row>
</div>
)
}
}
export default OfflineCourseFilter;
.video-course-filter {
position: relative;
.video-list-table{
// tr:nth-child(even){
// background: transparent !important;
// }
// tr:nth-child(odd){
// td{
// background: #FAFAFA !important;
// }
// }
}
.search-condition {
width: calc(100% - 80px);
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
margin-bottom: 12px;
align-items: center;
display: flex;
.search-name{
vertical-align: middle;
}
.shelf-status{
width:84px;
display:inline-block;
text-align:right;
}
}
}
.reset-fold-area {
position: absolute;
right: 12px;
}
.resetBtn {
color: #999999;
font-size: 18px;
margin-right: 8px;
}
.fold-btn {
font-size: 14px;
color: #666666;
line-height: 20px;
.fold-icon {
font-size: 12px;
margin-left:4px;
}
}
}
.data-icon {
cursor: pointer;
}
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-27 16:24:47
* @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Table, Modal, message , Tooltip,Switch,Dropdown} from 'antd';
import { Route, withRouter } from 'react-router-dom';
import moment from 'moment';
import $ from 'jquery';
import { PageControl } from "@/components";
import { appId, shareUrl, LIVE_SHARE } from '@/domains/course-domain/constants';
import OfflineCourseData from '@/modules/course-manage/offline-course/OfflineCourseData';
import ShareLiveModal from '@/modules/course-manage/modal/ShareLiveModal';
import Service from "@/common/js/service";
import CourseService from "@/domains/course-domain/CourseService";
import RelatedPlanModal from '../../modal/RelatedPlanModal';
import PreviewOfflineModal from '../modal/PreviewOfflineModal';
import ENUM from "../../../knowledge-base/ENUM.js";
import User from '@/common/js/user'
import QRCodeModal from '../modal/QRCodeModal';
import './OfflineCourseList.less';
const ENV = process.env.DEPLOY_ENV || 'dev';
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class OfflineCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
id: '', // 视频课ID
studentIds: [],
}
}
componentDidMount() {
const videoCourseItem = localStorage.getItem('videoCourseItem');
if (videoCourseItem) {
const _videoCourseItem = JSON.parse(videoCourseItem);
this.handleShowShareModal(_videoCourseItem, true);
}
}
handlePlanName = (planArray)=>{
let planStr = "";
planArray.map((item,index)=>{
if(index < planArray.length-1){
planStr = planStr + item.planName + '、';
}else{
planStr = planStr + item.planName
}
})
return planStr
}
// 请求表头
parseColumns = () => {
const columns = [
{
title: '线下课',
key: 'scheduleName',
dataIndex: 'scheduleName',
width:321,
fixed: 'left',
render: (val, record) => {
const { courseMediaVOS, courseName, offlinePlace, calendarTime, startTime, endTime } = record;
const coverUrl = (_.find(courseMediaVOS, data => data.contentType === 'COVER') || {}).mediaUrl;
let isContinue = calendarTime.length > 1;
_.reduce(calendarTime, (a, b) => {
isContinue = isContinue && (b - a === 86400000);
return b;
})
const lastTime = _.last(calendarTime);
const time = `${!isContinue ? calendarTime.map(item => moment(item).format('MM-DD')).join('、') : `${moment(calendarTime[0]).format('MM-DD')}~${moment(lastTime).format('MM-DD')}`} ${moment(startTime).format('HH:mm')} ~ ${moment(endTime).format('HH:mm')}`;
return (
<div className="record__item">
<img className="course-cover" src={coverUrl || defaultCoverUrl} />
<div style={{ width: 175 }}>
<Tooltip title={courseName}>
<div className="course-name">{courseName}</div>
</Tooltip>
<Tooltip title={`地点:${offlinePlace}`}>
<div className="course-text">地点:{offlinePlace}</div>
</Tooltip>
<Tooltip title={time}>
<div className="course-text">{time}</div>
</Tooltip>
</div>
</div>
)
}
},
{
title: '课程状态',
key: 'courseState',
dataIndex: 'courseState',
width: 120,
render: (val, record) => {
return (
<div className="course-state">
<div style={{ width: 6, height: 6, borderRadius: '50%', background: ENUM.offlineStateShow[val].color, marginRight: 8 }}></div>
{ENUM.offlineStateShow[val].title}
</div>
)
}
},
{
title: '课程分类',
key: 'categoryName',
dataIndex: 'categoryName',
width: 120,
render: (val, record) => {
return (
<div className="record__item">
{record.categorySonName}
</div>
)
}
},
{
title: <span>
<span>学院展示</span>
<Tooltip title={<div>开启后,学员可在学院内查看到此课程。若课程“取消”,则系统会自动“关闭”学院展示。<br/>关闭后,学院内不再展示此课程,但学员仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
width: 120,
dataIndex: "courseware",
render: (val, item, index) => {
return (
<Switch
disabled={item.courseState === 'EXPIRED'}
checked={item.shelfState === "YES"}
onChange={() => this.changeShelfState(item)}
/>
)
},
},
{
title: "讲师",
width: 200,
key: "teacher",
dataIndex: "teacher",
render: (val, item) => {
return (
<div>{item.teacherName}</div>
)
},
},
{
title: '报名时间',
width: 181,
key: 'apply',
dataIndex: 'apply',
sorter: true,
render: (val, item) => {
return <div>
{item.startTimeApply ? `${formatDate('MM-DD H:i', item.startTimeApply)} ~ ${formatDate('MM-DD H:i', item.endTimeApply)}` : '-'}
{item.whetherApplyFull === 'YES' &&
<span
style={{
marginLeft: 4,
color: '#FF4F4F',
background: 'rgba(255, 79, 79, 0.1)',
padding: '0 8px',
fontSize: '12px',
height: '18px',
lineHeight: '18px',
}}>已报满</span>
}
</div>
}
},
{
title: '创建时间',
width: 181,
key: 'created',
dataIndex: 'created',
sorter: true,
render: (val) => {
return formatDate('YYYY-MM-DD H:i', val)
}
},
{
title: '操作',
key: 'operate',
dataIndex: 'operate',
width: 210,
fixed: 'right',
render: (val, record) => {
return (
<div className="operate">
<div className="operate__item" onClick={() => window.RCHistory.push(`/offline-course/data?id=${record.courseId}`)}>参与数据</div>
{record.courseState !== 'EXPIRED' && <span className="operate__item split"> | </span>}
{record.courseState !== 'EXPIRED' && <div className="operate__item" onClick={() => this.handleShowShareModal(record)}>分享</div>}
<span className="operate__item split"> | </span>
<Dropdown overlay={this.renderMoreOperate(record)}>
<span className="more-operate">
<span className="operate-text">更多</span>
<span
className="iconfont icon"
style={{ color: "#5289FA" }}
>
&#xe824;
</span>
</span>
</Dropdown>
</div>
)
}
}
];
return columns;
}
// 显示预览弹窗
handleShowPreviewModal = (courseId) => {
const previewOfflineModal = (
<PreviewOfflineModal
courseId={courseId}
close={() => {
this.setState({
previewOfflineModal: null
})
}}
/>
);
this.setState({ previewOfflineModal });
}
renderMoreOperate = (item) => {
return (
<div className="live-course-more-menu">
{item.courseState !== 'EXPIRED' && (item.whetherSetSignIn === 'YES' || item.whetherSetSignOut === 'YES') && <div
className="operate__item"
key="qrcode"
onClick={() => {
this.setState({ openQRCodeModal: true, qrcodeData: item })
}}
>考勤二维码</div>}
<div
className="operate__item"
key="preview"
onClick={() => {
this.handleShowPreviewModal(item.courseId);
}}
>预览</div>
{(User.getUserRole() === 'CloudManager' || User.getUserRole() === 'StoreManager') && (item.courseState === 'UN_START' || item.courseState === 'STARTING') && <div
className="operate__item"
key="cancel"
onClick={() => {
this.handleDeleteOfflineCourse(item.courseId);
}}
>取消课程</div>}
{item.courseState === 'UN_START' && window.ctx.xmState.storeUserPermissionList.includes('EditOfflineClass') && <div
className="operate__item"
key="edit"
onClick={() => {
RCHistory.push(`/create-offline-course?type=edit&id=${item.courseId}`);
}}
>编辑</div>}
{(User.getUserRole() === 'CloudManager' || User.getUserRole() === 'StoreManager') && <div
className="operate__item"
key="delete"
onClick={() => this.handleDeleteOfflineCourse(item.courseId, true)}
>删除</div>}
</div>
)
}
//改变上架状态
changeShelfState = (item) => {
if (!window.ctx.xmState.storeUserPermissionList.includes('EditOfflineClass')) {
message.warning('无【编辑线下课】权限,请联系管理员')
return;
}
let _shelfState = item.shelfState
if(_shelfState==='NO'){
_shelfState = "YES";
item.shelfState = "YES"
}else{
_shelfState = "NO"
item.shelfState = "NO"
}
const params={
courseId: item.courseId,
shelfState:_shelfState
}
CourseService.changeVideoShelfState(params).then((res)=>{
if(res.success){
if(_shelfState === "YES"){
message.success("已开启展示");
}else{
message.success("已取消展示");
}
this.props.onChange();
}
})
}
// 删除视频课
handleDeleteOfflineCourse = (courseId, isDelete) => {
if (isDelete) {
Modal.confirm({
title: '删除课程',
content: '确定删除该课程吗?删除后学员数据和课程数据将无法恢复。',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: () => {
const param ={
courseId: courseId,
}
Service.Hades('public/hades/delOfflineCourse', param).then(() => {
message.success('删除成功');
this.props.onChange();
})
}
});
} else {
Modal.confirm({
title: '取消课程',
content: '确定要取消该课程吗?取消后课程将失效,仅保留已有数据。',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
okText: '确定',
cancelText: '取消',
onOk: () => {
const param ={
courseId: courseId,
}
Service.Hades('public/hades/cancelOfflineCourse', param).then(() => {
message.success('取消成功');
this.props.onChange();
})
}
});
}
}
// 显示分享弹窗
handleShowShareModal = (record, needStr = false) => {
const { courseId } = record;
const _appId = appId;
const htmlUrl = `${LIVE_SHARE}offline_detail/${courseId}?id=${User.getStoreId()}`;
const longUrl = htmlUrl;
const { courseName, courseMediaVOS } = record;
const coverUrl = (_.find(courseMediaVOS, data => data.contentType === 'COVER') || {}).mediaUrl;
const shareData = {
longUrl,
coverUrl,
courseName,
};
const shareLiveModal = (
<ShareLiveModal
needStr={needStr}
data={shareData}
type="videoClass"
title="线下课"
close={() => {
this.setState({
shareLiveModal: null
});
localStorage.setItem('videoCourseItem', '');
}}
/>
);
this.setState({ shareLiveModal });
}
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
const { query } = this.props;
let _columnKey;
let _order;
// 按创建时间升序排序
if (columnKey === 'apply' && order === 'ascend') {_columnKey="START_APPLY_DESC"; _order = 'SORT_ASC'; }
if (columnKey === 'created' && order === 'ascend') {_columnKey="CREATED"; _order = 'SORT_ASC'; }
// 按创建时间降序排序
if (columnKey === 'apply' && order === 'descend') { _columnKey="START_APPLY_DESC"; _order = 'SORT_DESC';}
if (columnKey === 'created' && order === 'descend') { _columnKey="CREATED"; _order = 'SORT_DESC';}
const _query = {
...query,
sortMap:{}
};
_query.sortMap[_columnKey]=_order;
this.props.onChange(_query);
}
render() {
const { openQRCodeModal, qrcodeData } = this.state;
const { dataSource = [], totalCount, query, match } = this.props;
const { current, size } = query;
return (
<div className="offline-course-list">
<Table
rowKey={record => record.courseId}
dataSource={dataSource}
columns={this.parseColumns()}
onChange={this.handleChangeTable}
pagination={false}
scroll={{ x: 1500}}
bordered
className="offline-list-table"
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={totalCount}
toPage={(page) => {
const _query = {...query, current: page + 1};
this.props.onChange(_query)
}}
/>
</div>
{this.state.shareLiveModal}
{this.state.previewOfflineModal}
<QRCodeModal
visible={openQRCodeModal}
data={qrcodeData}
onCancel={() => {
this.setState({ openQRCodeModal: false })
}}
/>
<Route path={`${match.url}/data`} component={OfflineCourseData} />
</div>
)
}
}
export default withRouter(OfflineCourseList);
.offline-course-list {
margin-top: 12px;
.offline-list-table{
tbody {
tr{
&:nth-child(even){
background: transparent !important;
td{
background:#FFF !important;
}
}
&:nth-child(odd){
background: #FAFAFA !important;
td{
background: #FAFAFA !important;
}
}
&:hover{
td{
background:#F3f6fa !important;
}
}
}
}
}
.watchUserCount{
text-align:right;
padding:16px;
}
.operate-text {
color: #5289FA;
cursor: pointer;
}
.operate {
display: flex;
&__item {
color: #5289FA;
cursor: pointer;
&.split {
margin: 0 8px;
color: #BFBFBF;
}
}
}
.more-operate{
line-height:20px;
}
.course-state {
display: flex;
align-items: center;
}
.record__item {
display: flex;
.course-cover {
min-width: 116px;
max-width: 116px;
height: 60px;
border-radius: 2px;
margin-right: 8px;
background-color: #000;
}
.course-name {
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
line-height: 20px;
margin-bottom: 4px;
}
.course-text {
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 20px;
font-size: 12px;
}
}
}
.offline-course-more-menu {
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 4px;
div {
line-height: 30px;
padding: 0 15px;
cursor: pointer;
&:hover {
background: #f3f6fa;
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:15
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-26 16:07:27
* @Description: 视频课-操作模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Button } from 'antd';
import './OfflineCourseOpt.less';
export default function OfflineCourseOpt() {
return (
<div className="video-course-opt">
<Button
type="primary"
onClick={() => {
RCHistory.push('/create-offline-course?type=add');
}}
className="mr12"
>新建线下课</Button>
</div>
);
}
.video-course-opt {
margin-top:4px;
.link {
color: #FF8534;
}
}
\ No newline at end of file
import React from 'react';
import OfflineCourseFilter from './components/OfflineCourseFilter';
import OfflineCourseOpt from './components/OfflineCourseOpt';
import OfflineCourseList from './components/OfflineCourseList';
import Service from '@/common/js/service';
import User from '@/common/js/user'
class OfflineCoursePage extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
size: 10,
current: 1,
storeId:User.getStoreId()
},
dataSource: [], // 线下课列表
totalCount: 0, // 线下课数据总条数
}
}
componentWillMount() {
// 获取线下课列表
this.handleFetchScheduleList();
}
// 获取线下课列表
handleFetchScheduleList = (_query = {}) => {
const query = {
...this.state.query,
..._query
};
// 更新请求参数
this.setState({ query });
Service.Hades('public/hades/getOfflineCoursePage', query).then((res) => {
const { result = {} } = res || {};
const { records = [], total = 0 } = result;
this.setState({
dataSource: records,
totalCount: Number(total)
});
})
}
render() {
const { dataSource, totalCount, query } = this.state;
return (
<div className="page video-course-page">
<div className="content-header">线下课</div>
<div className="box">
{/* 搜索模块 */}
<OfflineCourseFilter
onChange={this.handleFetchScheduleList}
/>
{/* 操作模块 */}
{window.ctx.xmState.storeUserPermissionList.includes('AddOfflineClass') && <OfflineCourseOpt />}
{/* 线下课列表模块 */}
<OfflineCourseList
query={query}
dataSource={dataSource}
totalCount={totalCount}
onChange={this.handleFetchScheduleList}
/>
</div>
</div>
)
}
}
export default OfflineCoursePage;
import React from 'react';
import { Modal } from 'antd';
import moment from 'moment';
import $ from 'jquery';
import Service from "@/common/js/service";
import './PreviewOfflineModal.less';
import ENUM from '@/modules/knowledge-base/ENUM';
const defaultCoverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class PreviewOfflineModal extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
componentDidMount() {
const { data } = this.props;
_.isEmpty(data) ? this.getCourseDetail() : this.setState({ ...data });
}
getCourseDetail = () => {
Service.Hades('public/hades/getOfflineCourseDetail', {
courseId: this.props.courseId,
}).then((res) => {
if (res.success) {
const { result } = res;
let coverUrl = '';
result.courseMediaVOS.map((item) => {
switch (item.contentType){
case "COVER":
coverUrl = item.mediaUrl;
break;
case "INTRO":
this.getTextDetail('introduce', item.mediaUrl);
break;
default:
break;
}
return item;
})
this.setState({
...result,
coverUrl,
})
}
})
}
getTextDetail = (key, url) => {
$.ajax({
data: {},
type: 'GET',
url,
contentType:'application/x-www-form-urlencoded; charset=UTF-8',
success: (res) => {
this.setState({ [key]: res, [`load${key}`]: true });
}
})
}
render() {
const {
coverUrl,
courseName,
teacherName,
courseState,
categoryName,
introduce,
startTime,
endTime,
offlinePlace,
whetherSetApply,
startTimeApply,
endTimeApply,
whetherSetSignIn,
whetherSetSignOut,
signInType,
signOutType,
signInTimeNum,
signInTimeUnit,
signOutStartTimeNum,
signOutStartTimeUnit,
signOutEndTimeNum,
signOutEndTimeUnit,
} = this.state;
const unit = (signInTimeUnit || '').toLocaleLowerCase() + 's';
const time = (signInType == 'START_AGO' ? startTime : endTime) && moment(signInType == 'START_AGO' ? startTime : endTime).subtract(signInTimeNum, unit);
const signInTime = (time && signInTimeNum) && `${moment(time).format('HH:mm')} ~ ${moment(signInType == 'START_AGO' ? startTime : endTime).format('HH:mm')}`;
const endUnit = (signOutEndTimeUnit || '').toLocaleLowerCase() + 's';
const end = (endTime && signOutEndTimeNum) && moment(endTime).add(signOutEndTimeNum, endUnit);
let startUnit = '';
let start = '';
if (signOutType === 'START_LATER') {
startUnit = (signOutStartTimeUnit || '').toLocaleLowerCase() + 's';
start = (startTime && signOutStartTimeNum) && moment(startTime).add(signOutStartTimeNum, startUnit);
}
console.log(start, end, startTime, endTime, 666666)
const signOutTime = (start || end) && (signOutType === 'START_LATER' ? `${moment(start).format('HH:mm')} ~ ${moment(end).format('HH:mm')}` : `${moment(endTime).format('HH:mm')} ~ ${moment(end).format('HH:mm')}`);
return (
<Modal
title="预览"
visible={true}
width={680}
onCancel={this.props.close}
footer={null}
maskClosable={false}
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
className="preview-live-graphics-modal"
>
<div className="container__wrap">
<div className="container">
<div className="container__header">
<div className="cover"></div>
<img src={coverUrl || defaultCoverUrl} className="course-cover" />
<span className="cover-teacher">主讲人:{teacherName}</span>
<span className="cover-state" style={{ background: (ENUM.offlineStateShow[courseState] || {}).color }}>{(ENUM.offlineStateShow[courseState] || {}).title}</span>
</div>
<div className="container__body">
<div className="title__name">{courseName}</div>
<div className="title__categery">
<img className="item-icon" src="https://image.xiaomaiketang.com/xm/cDMwz3jzaX.png" />
课程分类:{categoryName}
</div>
<div className="title__categery">
<img className="item-icon" src="https://image.xiaomaiketang.com/xm/BfTBK3dGda.png" />
上课时间:{startTime ? moment(startTime).format('HH:mm') : ''}~{endTime ? moment(endTime).format('HH:mm') : ''}
</div>
<div className="title__categery">
<img className="item-icon" src="https://image.xiaomaiketang.com/xm/KhFNBTtAKR.png" />
上课地点:{offlinePlace}
</div>
</div>
<div className="container__introduction">
<div className="title">线下课简介</div>
<div className="container__introduction__list editor-box">
{whetherSetApply === 'YES' && <div className="course-time">
<div className="time-title">报名时间</div>
<div className="time-text">{startTimeApply && moment(startTimeApply).format('MM-DD HH:mm')} ~ {endTimeApply && moment(endTimeApply).format('MM-DD HH:mm')}</div>
</div>}
{(whetherSetSignIn === 'YES' || whetherSetSignOut === 'YES') && <div className="course-time">
<div className="time-title">考勤时间</div>
{whetherSetSignIn === 'YES' &&
<div className="time-text">
签到:{signInTime}
</div>
}
{whetherSetSignOut === 'YES' &&
<div className="time-text">
签退:{signOutTime}
</div>
}
</div>}
<div
className="intro-item text"
dangerouslySetInnerHTML={{
__html: introduce
}}
/>
</div>
</div>
</div>
</div>
</Modal>
)
}
}
export default PreviewOfflineModal;
.preview-live-graphics-modal {
.ant-modal-body {
background-image: url('https://image.xiaomaiketang.com/xm/xZWdziTCAf.png');
background-size: 100% 100%;
}
.container__wrap {
width: 340px;
height: 618px;
padding: 67px 46px 48px 47px;
margin: auto;
background-image: url('https://image.xiaomaiketang.com/xm/DHMzHiGc2E.png');
background-size: 100% 100%;
}
.container {
overflow: scroll;
height: 100%;;
&__header {
position: relative;
.cover {
position: absolute;
width: 100%;
height: 36px;
left: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); z-index: 10;
}
.course-cover, .course-url {
width: 100%;
height: 141px;
background: #000;
}
.cover-teacher {
position: absolute;
bottom: 8px;
left: 16px;
font-size: 10px;
color: #fff;
z-index: 10;
}
.cover-state {
position: absolute;
bottom: 8px;
right: 16px;
font-size: 12px;
height: 18px;
line-height: 18px;
padding: 0 8px;
color: #fff;
border-radius: 9px;
z-index: 10;
}
}
&__body {
background-color: #FFF;
padding: 7px 0 11px 0;;
.title__name {
color: #333333;
font-weight: 500;
font-size: 12px;
}
.title__categery {
font-size: 10px;
color: #999999;
display: flex;
align-items: center;
.item-icon {
width: 12px;
height: 12px;
margin-right: 4px;
font-size: 10px;
}
}
}
&__introduction {
margin-top: 10px;
padding: 12px 0;
position: relative;
&::after {
content: '';
position: absolute;
width: 241px;
top: -10px;
height: 10px;
background: #F4F6FA;
}
.title {
position: relative;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #333333;
padding: 0 10px;
border: none !important;
&::before {
content: '';
position: absolute;
width: 12px;
height: 1px;
top: 12px;
left: 50%;
transform: translateX(-50px);
background: #ccc;
}
&::after {
content: '';
position: absolute;
width: 12px;
height: 1px;
top: 12px;
left: 50%;
transform: translateX(36px);
background: #ccc;
}
}
&__list {
margin-top: 8px;
.course-time {
margin: 0 16px 8px 0;
.time-title {
position: relative;
padding-left: 6px;
font-size: 12px;
color: #333;
line-height: 18px;
&::before {
position: absolute;
content: '';
width: 2px;
height: 14px;
background: #FFB714;
left: 0;
top: 2px;
}
}
.time-text {
padding-left: 6px;
font-size: 12px;
color: #999999;
line-height: 18px;
.time-tag {
padding: 0 8px;
font-size: 12px;
height: 18px;
border-radius: 2px;
background: rgba(32, 206, 205, 0.1);
color: #20CECD;
margin-left: 8px;
&.apply {
color: #FF4F4F;
background: rgba(255, 79, 79, 0.1);
}
}
}
}
.intro-item:not(:first-child) {
margin-top: 13px;
}
color: #666;
p {
font-size: 12px;
}
img {
max-width: 100%;
}
}
}
}
}
\ No newline at end of file
import React from 'react';
import { Button, Modal, Select } from 'antd';
import moment from 'moment';
import html2canvas from 'html2canvas';
import { LIVE_SHARE } from "@/domains/course-domain/constants";
import QRCode from '../../../../libs/qrcode/qrcode';
import User from '@/common/js/user';
import Service from "@/common/js/service";
import ScanFileModal from '../../../resource-disk/modal/ScanFileModal';
import './QRCodeModal.less';
const { Option } = Select;
export default class QRCodeModal extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedDate: '',
data: {},
imgUrl1: '',
imgUrl2: '',
showPreviewModal: false,
}
}
componentDidUpdate(preProps) {
const { visible } = this.props;
if (!preProps.visible && visible) {
this.getCourseDetail();
}
}
getQrcode = () => {
const { selectedDate, data } = this.state;
let list = [];
const qrcodeWrapDom1 = document.querySelector('#qrcodeWrap1');
const qrcodeWrapDom2 = document.querySelector('#qrcodeWrap2');
if (data.whetherSetSignIn === 'YES') {
const htmlUrl = `${LIVE_SHARE}sign_in?id=${User.getStoreId()}&calendar=${selectedDate}&courseId=${data.courseId}&from=work_weixin`;
list.push(htmlUrl);
}
if (data.whetherSetSignOut === 'YES') {
const htmlUrl = `${LIVE_SHARE}sign_out?id=${User.getStoreId()}&calendar=${selectedDate}&courseId=${data.courseId}&from=work_weixin`;
list.push(htmlUrl);
}
Service.Sales('public/businessShow/convertShortUrls', {
urls: list
}).then((res) => {
const { result = [] } = res;
const qrcodeNode1 = qrcodeWrapDom1 && new QRCode({
text: result[0].shortUrl,
size: 170,
});
const qrcodeNode2 = qrcodeWrapDom2 && new QRCode({
text: result[data.whetherSetSignIn === 'YES' ? 1 : 0].shortUrl,
size: 170,
});
qrcodeWrapDom1 && qrcodeWrapDom1.childNodes[0] && qrcodeWrapDom1.removeChild(qrcodeWrapDom1.childNodes[0]);
qrcodeWrapDom1 && qrcodeWrapDom1.appendChild(qrcodeNode1);
qrcodeWrapDom2 && qrcodeWrapDom2.childNodes[0] && qrcodeWrapDom2.removeChild(qrcodeWrapDom2.childNodes[0]);
qrcodeWrapDom2 && qrcodeWrapDom2.appendChild(qrcodeNode2);
setTimeout(() => {
this.initQRCode();
}, 300)
})
}
getCourseDetail = () => {
const { courseId } = this.props.data;
Service.Hades('public/hades/getOfflineCourseDetail',{
courseId
}).then((res) => {
this.setState({ selectedDate: res.result.calendarTime[0], data: res.result }, () => {
this.getQrcode();
})
});
}
initQRCode = () => {
const domList = document.querySelectorAll('.image-box');
for (let index = 0; index < domList.length; index++) {
const dom = domList[index];
html2canvas(dom, {
useCORS: true,
}).then(canvas => {
const dataUrl = canvas.toDataURL('image/png');
this.setState({ [`imgUrl${index + 1}`]: dataUrl });
});
}
}
downloadImg = () => {
const { imgUrl1, imgUrl2, selectedDate, data } = this.state;
let fileName1 = `签到二维码_${data.courseName}_${moment(selectedDate).format('YYYY-MM-DD')}.png`;
let fileName2 = `签退二维码_${data.courseName}_${moment(selectedDate).format('YYYY-MM-DD')}.png`;
imgUrl1 && setTimeout(() => window.downloadFile(imgUrl1, fileName1), 0);
imgUrl2 && setTimeout(() => window.downloadFile(imgUrl2, fileName2), 0);
}
render() {
const { visible, onCancel } = this.props;
const { selectedDate, data = {}, imgUrl1, imgUrl2, imgUrl, showPreviewModal } = this.state;
const { courseName, whetherSetSignIn, whetherSetSignOut } = data;
const date = moment(selectedDate).format('YYYY-MM-DD');
const unit = (data.signInTimeUnit || '').toLocaleLowerCase() + 's';
const time = moment(`${date} ${moment(data.signInType == 'START_AGO' ? data.startTime : data.endTime).format('HH:mm')}`);
const signInTime = `${moment(time).subtract(data.signInTimeNum, unit).format('YYYY-MM-DD HH:mm')} ~ ${moment(time).format('YYYY-MM-DD HH:mm')}`;
const endUnit = (data.signOutEndTimeUnit || '').toLocaleLowerCase() + 's';
const end = moment(`${date} ${moment(data.endTime).format('HH:mm')}`).add(data.signOutEndTimeNum, endUnit);
let startUnit = '';
let start = '';
if (data.signOutType === 'START_LATER') {
startUnit = (data.signOutStartTimeUnit || '').toLocaleLowerCase() + 's';
start = moment(`${date} ${moment(data.startTime).format('HH:mm')}`).add(data.signOutStartTimeNum, startUnit);
}
const signOutTime = data.signOutType === 'START_LATER' ? `${moment(start).format('YYYY-MM-DD HH:mm')} ~ ${moment(end).format('YYYY-MM-DD HH:mm')}` : `${moment(`${date} ${moment(data.endTime).format('HH:mm')}`).format('YYYY-MM-DD HH:mm')} ~ ${moment(end).format('YYYY-MM-DD HH:mm')}`;
return (
<Modal
title="考勤二维码"
width={560}
visible={visible}
footer={<Button onClick={() => onCancel()}>关闭</Button>}
onCancel={() => onCancel()}
className="offline-qrcode-modal"
>
<div>
<Select
style={{ width: 200 }}
placeholder="请选择"
value={selectedDate}
onChange={(value) => {
this.setState({ selectedDate: value }, () => {
this.getQrcode(this.props);
})
}}
>
{(data.calendarTime || []).map(item => (
<Option value={item} key={item}>{moment(item).format('MM月DD日')}</Option>
))}
</Select>
<Button
type="primary"
style={{ marginLeft: 8 }}
onClick={() => {
this.downloadImg();
}}
>下载二维码</Button>
</div>
{whetherSetSignIn === 'YES' && <div
className="image-box"
key="1"
onClick={() => this.setState({ showPreviewModal: true, imgUrl: imgUrl1 })}
>
<img src="https://image.xiaomaiketang.com/xm/2YHQEYrWBD.png" className="image" />
<div className="title">签到二维码</div>
<div className="name" onClick={e => e.stopPropagation()}>{courseName}</div>
<div className="qrcodeWrap" id="qrcodeWrap1"></div>
<div className="time" onClick={e => e.stopPropagation()}>签到时间:{signInTime}</div>
</div>}
{whetherSetSignOut === 'YES' && <div
className="image-box"
key="2"
onClick={() => this.setState({ showPreviewModal: true, imgUrl: imgUrl2 })}
>
<img src="https://image.xiaomaiketang.com/xm/2YHQEYrWBD.png" className="image" />
<div className="title">签退二维码</div>
<div className="name" onClick={e => e.stopPropagation()}>{courseName}</div>
<div className="qrcodeWrap" id="qrcodeWrap2"></div>
<div className="time" onClick={e => e.stopPropagation()}>签退时间:{signOutTime}</div>
</div>}
{showPreviewModal &&
<ScanFileModal
fileType="JPG"
item={{ ossUrl: imgUrl }}
close={() => this.setState({ showPreviewModal: false })}
/>
}
</Modal>
)
}
}
\ No newline at end of file
.offline-qrcode-modal {
.ant-modal-body {
max-height: 483px!important;
}
.image-box {
margin: 16px auto 0;
width: 380px;
height: 350px;
position: relative;
.image {
width: 100%;
height: 100%;
object-fit: contain;
box-shadow: 0px 0px 30px 0px rgba(128, 128, 128, 0.1);
border-radius: 6px;
}
.title {
position: absolute;
font-size: 20px;
font-weight: 500;
top: 24px;
left: 50%;
transform: translateX(-50%);
color: #fff;
}
.name {
font-size: 14px;
color: #fff;
position: absolute;
top: 58px;
left: 50%;
transform: translateX(-50%);
width: 286px;
line-height: 20px;
text-align: center;
}
.qrcodeWrap {
width: 186px;
height: 186px;
left: 50%;
transform: translateX(-50%);
position: absolute;
top: 100px;
padding: 8px;
background: 8px;
border-radius: 4px;
background: #fff;
}
.time {
font-size: 12px;
color: #333;
padding: 0 8px;
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
line-height: 30px;
height: 30px;
border-radius: 15px;
background: #fff;
text-align: center;
}
}
}
\ No newline at end of file
......@@ -121,8 +121,8 @@ class AddVideoIntro extends React.Component {
</div>
<div>
<div className="desc">
<div>开启:允许未绑定手机号的用户观看</div>
<div>关闭:仅限绑定了手机号的用户可以进入观看视频</div>
<div>开启:允许未绑定手机号的学员观看</div>
<div>关闭:仅限绑定了手机号的学员可以进入观看视频</div>
</div>
</div>
</div>
......@@ -136,8 +136,8 @@ class AddVideoIntro extends React.Component {
</Col>
<Col span={21}>
<div className="desc">
<div>开启:此视频将在用户学院的视频列表中出现</div>
<div>关闭:此视频将在用户学院的视频列表中隐藏</div>
<div>开启:此视频将在学员学院的视频列表中出现</div>
<div>关闭:此视频将在学员学院的视频列表中隐藏</div>
</div>
</Col>
</Row>
......
/*
* @Author: 吴文洁
* @Date: 2020-08-05 10:12:45
* @LastEditors: yuananting
* @LastEditTime: 2021-03-24 19:34:48
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:34:11
* @Description: 视频课-列表模块
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
......@@ -126,7 +126,7 @@ class VideoCourseList extends React.Component {
{
title: <span>
<span>学院展示</span>
<Tooltip title={<div>开启后,用户可在学院内查看到此课程。若课程“未成功开课”,则系统会自动“关闭”学院展示。<br/>关闭后,学院内不再展示此课程,但用户仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
<Tooltip title={<div>开启后,学员可在学院内查看到此课程。<br/>关闭后,学院内不再展示此课程,但学员仍可通过分享的海报/链接查看此课程。</div>}><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
width: 120,
dataIndex: "courseware",
......@@ -137,7 +137,7 @@ class VideoCourseList extends React.Component {
},
},
{
title: "观看用户数",
title: "观看学员数",
width: 110,
key: "watchUserCount",
dataIndex: "watchUserCount",
......
......@@ -84,7 +84,7 @@ class WatchDataModal extends React.Component {
parseColumns = () => {
const columns = [
{
title: '观看用户',
title: '观看学员',
key: 'name',
dataIndex: 'name'
},
......@@ -133,7 +133,7 @@ class WatchDataModal extends React.Component {
closeIcon={<span className="icon iconfont modal-close-icon">&#xe6ef;</span>}
>
<div className="search-container">
<Search placeholder="搜索用户姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
<Search placeholder="搜索学员姓名/手机号" style={{ width: 200 }} onChange={(e) => { this.handleChangNickname(e.target.value)}} onSearch={ () => { this.handleFetchDataList()}} enterButton={<span className="icon iconfont">&#xe832;</span>}/>
</div>
<div>
<Table
......
......@@ -102,11 +102,13 @@ class Home extends React.Component {
incLiveCourseNum: res.result.incLiveCourseNum,
incVideoCourseNum: res.result.incVideoCourseNum,
incPictureCourseNum: res.result.incPictureCourseNum,
incOfflineCourseNum: res.result.incOfflineCourseNum,
incTrainingPlanNum: res.result.incTrainingPlanNum,
liveCourseNum: res.result.liveCourseNum,
totalCustomerNum: res.result.totalCustomerNum,
videoCourseNum: res.result.videoCourseNum,
pictureCourseNum: res.result.pictureCourseNum,
offlineCourseNum: res.result.offlineCourseNum,
trainingPlanNum: res.result.trainingPlanNum,
})
}
......@@ -231,6 +233,8 @@ class Home extends React.Component {
unfinishedNum,
completeNum,
planCustomerNum,
incOfflineCourseNum,
offlineCourseNum,
} = this.state;
const data = [
{
......@@ -266,7 +270,7 @@ class Home extends React.Component {
<div className="data-item">
<div className="header">
<img className="header-icon" src="https://image.xiaomaiketang.com/xm/wAaFtjeRsM.png" />
<span className="header-word">用户总数 (人)</span>
<span className="header-word">学员总数 (人)</span>
</div>
<div className="data-number">{totalCustomerNum}</div>
<div className="data-footer">
......@@ -320,12 +324,12 @@ class Home extends React.Component {
<div className="course-item">
<div className="course-title">线下课</div>
<div className="data">
<span className="course-number">0</span>
<span className="course-number">{offlineCourseNum}</span>
<span className="course-word">本月新增</span>
{false &&
{incOfflineCourseNum > 0 &&
<span className="icon iconfont">&#xe635;</span>
}
<span className="add-number">0</span>
<span className="add-number">{incOfflineCourseNum}</span>
</div>
</div>
</div>
......
......@@ -115,6 +115,7 @@
background: #fff;
width: ~'calc(50% - 8px)';
padding: 16px;
overflow: hidden;
.study-title {
font-size: 16px;
color: #333;
......
......@@ -30,6 +30,28 @@ const ENUM = {
color: "#999",
},
},
offlineStateShow: {
UN_START: {
code: 1,
title: "未开始",
color: "#FDBE31",
},
STARTING: {
code: 2,
title: "进行中",
color: "#238FFF",
},
FINISH: {
code: 3,
title: "已结束",
color: "#2FC83C",
},
EXPIRED: {
code: 4,
title: "已取消",
color: "#CCCCCC",
},
},
CourseTypeEnum: {
LIVE: "直播课",
......
......@@ -2,8 +2,8 @@
* @Description:
* @Author: zangsuyun
* @Date: 2021-03-15 16:51:40
* @LastEditors: zangsuyun
* @LastEditTime: 2021-04-10 16:13:07
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:34:39
* @Copyright: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -116,7 +116,7 @@ class CourseData extends React.Component {
getStudentColumns() {
const columns = [
{
title: "用户姓名",
title: "学员姓名",
dataIndex: "userName",
},
{
......@@ -314,9 +314,9 @@ class CourseData extends React.Component {
</div>
</div>
{/* 用户上课数据 */}
{/* 学员上课数据 */}
<div className="courseData-student">
<p className="title">用户上课数据</p>
<p className="title">学员上课数据</p>
<div className="filter-wrap">
<div className="filter">
<Button
......
......@@ -2,8 +2,8 @@
* @Description:
* @Author: zangsuyun
* @Date: 2021-03-12 14:49:40
* @LastEditors: zangsuyun
* @LastEditTime: 2021-04-12 10:31:53
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:35:41
* @Copyright: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -251,7 +251,7 @@ class KnowledgeBaseList extends React.Component {
{record.hideToUser && (
<Tooltip
title={
<div>课程未成功开课,已在用户知识列表中隐藏</div>
<div>课程未成功开课,已在学员知识列表中隐藏</div>
}
>
<i
......@@ -423,7 +423,7 @@ class KnowledgeBaseList extends React.Component {
},
},
{
title: "观看用户数",
title: "观看学员数",
key: "watchUserCount",
dataIndex: "watchUserCount",
align:'right',
......
......@@ -56,7 +56,7 @@ class PlaybackData extends React.Component {
getPlaybackColumns() {
const columns = [
{
title: "观看用户",
title: "观看学员",
dataIndex: "userName",
},
{
......
......@@ -2,8 +2,8 @@
* @Description:
* @Author: zangsuyun
* @Date: 2021-03-16 10:18:31
* @LastEditors: zangsuyun
* @LastEditTime: 2021-03-30 10:17:59
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:36:00
* @Copyright: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -101,7 +101,7 @@ class WatchDataModal extends React.Component {
const { type } = this.props;
const columns = [
{
title: "观看用户",
title: "观看学员",
key: "name",
dataIndex: "name",
},
......@@ -195,7 +195,7 @@ class WatchDataModal extends React.Component {
<div className="watch-data">
<div className="search-container">
<Search
placeholder="搜索用户姓名/手机号"
placeholder="搜索学员姓名/手机号"
style={{ width: 200 }}
onChange={(e) => {
this.handleChangNickname(e.target.value);
......
......@@ -102,7 +102,7 @@ class LearningData extends React.Component {
<Tabs.TabPane tab="员工分享数据" key="employeeShareData" forceRender="true">
<EmployeeShareData/>
</Tabs.TabPane>
<Tabs.TabPane tab="用户学习数据" key="userLearningData" forceRender="true">
<Tabs.TabPane tab="学员学习数据" key="userLearningData" forceRender="true">
<UserLearningData/>
</Tabs.TabPane>
</Tabs>)
......
/*
* @Author: zhangleyuan
* @Date: 2021-02-20 16:45:51
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-27 11:20:27
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:36:26
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -267,8 +267,8 @@ class BasicInfo extends React.Component{
</div>
<div>
<div className="instro-text">
<div>开启:此培训计划可以分享给用户进行学习</div>
<div>关闭:此培训计划暂不可分享给用户进行学习,后续可开启</div>
<div>开启:此培训计划可以分享给学员进行学习</div>
<div>关闭:此培训计划暂不可分享给学员进行学习,后续可开启</div>
</div>
</div>
</div>
......@@ -313,7 +313,7 @@ class BasicInfo extends React.Component{
<div>
<div className="live-standard-info">
<span className="icon iconfont">&#xe865;</span>
<span className="instro">直播课单个课程,用户学习进度达到
<span className="instro">直播课单个课程,学员学习进度达到
<Input
width="40"
value={percentCompleteLive}
......@@ -326,7 +326,7 @@ class BasicInfo extends React.Component{
</div>
<div className="live-standard-info">
<span className="icon iconfont">&#xe864;</span>
<span className="instro">视频课单个课程,用户学习进度达到
<span className="instro">视频课单个课程,学员学习进度达到
<Input
width="40"
value={percentCompleteVideo}
......@@ -339,7 +339,7 @@ class BasicInfo extends React.Component{
</div>
<div className="live-standard-info">
<span className="icon iconfont">&#xe601;</span>
<span className="instro">图文课单个课程,用户学习进度达到
<span className="instro">图文课单个课程,学员学习进度达到
<Input
width="40"
value={percentCompletePicture}
......@@ -350,19 +350,6 @@ class BasicInfo extends React.Component{
%,即视为"已完成"学习
</span>
</div>
{/* <div className="video-standard-info">
<span className="icon iconfont">&#xe864;</span>
<span>图文课单个课程,用户学习进度达到
<Input
width="40"
value={percentCompleteVideo}
onChange={(e) => { this.props.onChange('percentCompleteVideo', e.target.value.replace(/\D/g,'')) }}
onBlur={(e) => this.percentCompleteBlur(e, 'percentCompleteVideo')}
className="input-box"
/>
%,即视为“已完成”学习
</span>
</div> */}
</div>
</div>
{ operatorModalVisible &&
......
/*
* @Author: zhangleyuan
* @Date: 2021-02-20 16:46:46
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-03-16 15:53:59
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:36:43
* @Description: 描述一下
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -242,7 +242,7 @@ function PlanList(props) {
function handleDelete(record){
return confirm({
title: '你确定要删除吗?',
content: '删除后,此培训计划的用户将无法继续学习,所有学习数据将同步删除不可恢复',
content: '删除后,此培训计划的学员将无法继续学习,所有学习数据将同步删除不可恢复',
icon: <span className="icon iconfont default-confirm-icon">&#xe839; </span>,
okText: '删除',
okType: 'danger',
......
......@@ -110,8 +110,8 @@ class UserLearningData extends React.Component {
UnbundEmployee = (record)=>{
if(User.getUserRole()==='CloudOperator'){
return confirm({
title: "你确定要解绑与用户的关系吗?",
content: "解绑后,用户该培训计划的学习数据将同步移出",
title: "你确定要解绑与学员的关系吗?",
content: "解绑后,学员该培训计划的学习数据将同步移出",
icon: (
<span className="icon iconfont default-confirm-icon">&#xe839; </span>
),
......@@ -156,7 +156,7 @@ class UserLearningData extends React.Component {
if(User.getUserRole() === "CloudManager" || User.getUserRole() === "StoreManager"){
columns = [
{
title: '用户',
title: '学员',
key: 'storeCustomerName',
dataIndex: 'storeCustomerName',
render: (val, record) => {
......@@ -225,7 +225,7 @@ class UserLearningData extends React.Component {
{
title: <span>
<span>学习进度</span>
<Tooltip title="用户培训计划中达到“已完成”状态的课程数/总课程数"><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
<Tooltip title="学员培训计划中达到“已完成”状态的课程数/总课程数"><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
key: 'learnNum',
dataIndex: 'learnNum',
......@@ -259,7 +259,7 @@ class UserLearningData extends React.Component {
}else{
columns = [
{
title: '用户',
title: '学员',
key: 'storeCustomerName',
dataIndex: 'storeCustomerName',
render: (val, record) => {
......@@ -311,7 +311,7 @@ class UserLearningData extends React.Component {
{
title: <span>
<span>学习进度</span>
<Tooltip title="用户培训计划中达到“已完成”状态的课程数/总课程数"><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
<Tooltip title="学员培训计划中达到“已完成”状态的课程数/总课程数"><i className="icon iconfont" style={{ marginLeft: '5px',cursor:'pointer',color:'#bfbfbf',fontSize:'14px'}}>&#xe61d;</i></Tooltip>
</span>,
key: 'learnNum',
dataIndex: 'learnNum',
......
......@@ -126,10 +126,10 @@ function UserLearningDataFilter(props) {
<Row>
<div className="search-condition">
<div className="search-condition__item">
<span className="label customer-label">用户</span>
<span className="label customer-label">学员</span>
<Search
value={query.customerName}
placeholder="搜索用户名称"
placeholder="搜索学员名称"
onChange={(e) => { handleChangeQuery('customerName', e.target.value)}}
onSearch={ () => { props.onChange(query) } }
style={{ width: "calc(100% - 70px)" }}
......
......@@ -136,13 +136,13 @@ class ShareLiveModal extends React.Component {
<div className="right">
<div className="share-poster right__item">
<div className="title">① 海报分享</div>
<div className="sub-title">用户可通过微信扫描海报二维码,查看培训计划</div>
<div className="sub-title">学员可通过微信扫描海报二维码,查看培训计划</div>
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
</div>
<div className="share-url right__item">
<div className="title">② 链接分享</div>
<div className="sub-title">用户可通过微信或浏览器打开以下链接,查看培训计划</div>
<div className="sub-title">学员可通过微信或浏览器打开以下链接,查看培训计划</div>
<div className="content url-content">
<div className="share-url" id="shareUrl">{shareUrl}</div>
<Button type="primary" onClick={this.handleCopy}>复制</Button>
......
......@@ -163,7 +163,7 @@ class UserLearnDetailModal extends React.Component {
const { visible } = this.props;
return (
<Modal
title="用户学习详情"
title="学员学习详情"
onCancel={this.props.onClose}
onOk={this.props.onClose}
maskClosable={false}
......@@ -175,7 +175,7 @@ class UserLearnDetailModal extends React.Component {
>
<div className="customer-info">
<span className="customer-name">
<span>用户</span>
<span>学员</span>
<span>{storeCustomerName}</span>
</span>
<span className="customer-phone">
......
......@@ -288,7 +288,8 @@ class SelectPrepareFileModal extends React.Component {
const hasMore = folderList.length < totalCount;
const { fileListRef } = this.refs;
const hasReachBottom = fileListRef.scrollTop + fileListRef.clientHeight === fileListRef.scrollHeight;
// const hasReachBottom = fileListRef.scrollTop + fileListRef.clientHeight === fileListRef.scrollHeight;
const hasReachBottom = fileListRef.scrollTop + fileListRef.clientHeight > fileListRef.scrollHeight - 1;
if (!hasReachBottom || !hasMore) return;
const currentFolder = folderPathList[folderPathList.length - 1];
......
/*
* @Author: 吴文洁
* @Date: 2020-06-09 09:47:44
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-16 17:53:33
* @Last Modified by: chenshu
* @Last Modified time: 2021-05-08 10:46:44
* @Description: 文件夹管理
*/
......@@ -113,7 +113,6 @@ class FolderManage extends React.Component {
// 判断是否继承了父级文件权限
if (records.length > 0 && !records[0].rights) {
// debugger
records.map((item,index) => {
item.rights = parentRights;
_records.push(item);
......@@ -149,7 +148,6 @@ class FolderManage extends React.Component {
// 判断是否继承了父级文件权限
if (records.length > 0 && !records[0].rights) {
// debugger
records.map((item,index) => {
item.rights = _parentRights;
_records.push(item);
......
......@@ -23,6 +23,8 @@ export default class CollegeManagePage extends React.Component {
list: [],
enterpriseId: User.getEnterpriseId(),
isAdmin: false,
createStoreList:[],
joinStoreList:[]
};
}
......@@ -57,10 +59,15 @@ export default class CollegeManagePage extends React.Component {
User.setStoreId(mainStore.id);
User.setStoreUserId(mainStore.storeUserId);
}
this.setState({ list })
const createStoreList = list.filter((item)=>{
return item.userRole === 'StoreManager'
})
const joinStoreList = list.filter((item)=>{
return item.userRole !== 'StoreManager'
})
this.setState({createStoreList:createStoreList,joinStoreList:joinStoreList})
});
}
checkCollege(item, bool) {
const data = {
storeId: item.id,
......@@ -77,7 +84,7 @@ export default class CollegeManagePage extends React.Component {
Modal.confirm({
title: '确定停用吗?',
content: '停用学院,所有学院相关信息不可使用,用户无法继续学习,请谨慎操作!',
content: '停用学院,所有学院相关信息不可使用,学员无法继续学习,请谨慎操作!',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>,
onOk: () => {
this.changeCollege(item, bool);
......@@ -104,7 +111,7 @@ export default class CollegeManagePage extends React.Component {
}
handleLogout() {
BaseService.logout({}).then((res) => {
BaseService.logout({identifier:User.getIdentifier()}).then((res) => {
User.removeUserId();
User.removeToken();
User.removeEnterpriseId();
......@@ -120,6 +127,8 @@ export default class CollegeManagePage extends React.Component {
avatar,
list,
isAdmin,
createStoreList,
joinStoreList
} = this.state;
return (
<div className="college-manage-page">
......@@ -154,12 +163,13 @@ export default class CollegeManagePage extends React.Component {
<img className="image" src={avatar} />
<span className="name">{name}</span>
</div>
<div>
<div className="title-box">
<span className="title">企学院 ({list.length})</span>
{isAdmin && <span className="text">最多可创建10个企学院,您还能创建{10 - list.length}</span>}
<span className="title">我创建的({createStoreList.length})</span>
{isAdmin && <span className="text">最多可创建10个企学院,您还能创建{10 - createStoreList.length}</span>}
</div>
<div className="college-list">
{list.map((item) => (
{createStoreList.map((item) => (
<div
key={item.id}
className={`college-item${item.state === 'VALID' ? '' : ' disabled'}${item.userRole === 'StoreManager' ? ' enabled' : ''}`}
......@@ -179,7 +189,7 @@ export default class CollegeManagePage extends React.Component {
<div className="title">{item.storeName}</div>
<div className="time">{moment(item.createTime).format('YYYY-MM-DD HH:mm')}</div>
<div className="control-box">
{item.userRole === 'StoreManager' && item.state === 'VALID' && !item.mainStore &&
{item.userRole === 'StoreManager' && item.state === 'VALID' &&
<span
className="control-button"
onClick={(e) => {
......@@ -212,7 +222,7 @@ export default class CollegeManagePage extends React.Component {
</div>
</div>
))}
{list.length < 10 && isAdmin &&
{createStoreList.length < 10 && isAdmin &&
<div
className="college-create"
onClick={() => {
......@@ -224,6 +234,70 @@ export default class CollegeManagePage extends React.Component {
}
</div>
</div>
{ joinStoreList.length > 0 &&
<div className="join-container">
<div className="title-box">
<span className="title">我加入的({joinStoreList.length})</span>
</div>
<div className="college-list">
{joinStoreList.map((item) => (
<div
key={item.id}
className={`college-item${item.state === 'VALID' ? '' : ' disabled'}${item.userRole === 'StoreManager' ? ' enabled' : ''}`}
onClick={() => {
if (item.state !== 'VALID') {
message.warning('学院已停用,请启用后使用或联系学院管理员');
return null;
};
User.setStoreId(item.id);
window.RCHistory.push('/home')
}}
>
<div className="header">
<img className="image" src={item.logo || "https://image.xiaomaiketang.com/xm/fe4NCjr7XF.png"} />
<span className="tag">{roleMap[item.userRole]}</span>
</div>
<div className="title">{item.storeName}</div>
<div className="time">{moment(item.createTime).format('YYYY-MM-DD HH:mm')}</div>
<div className="control-box">
{item.userRole === 'StoreManager' && item.state === 'VALID' && !item.mainStore &&
<span
className="control-button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
User.setStoreId(item.id);
window.RCHistory.push('/college-info')
}}
>编辑</span>
}
{((item.userRole === 'StoreManager' && !item.mainStore) || item.state !== 'VALID') &&
<span
className="control-button"
onClick={(e) => {
if (item.state !== 'VALID') return null;
e.preventDefault();
e.stopPropagation();
this.checkCollege(item, false);
}}
>{item.state === 'VALID' ? '停用' : '已停用'}</span>
}
<span
className="control-button disable-button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
this.changeCollege(item, true);
}}
>启用</span>
</div>
</div>
))}
</div>
</div>
}
</div>
</div>
)
}
......
......@@ -48,6 +48,9 @@
width: 1280px;
margin: 0 auto;
padding: 60px 0 30px 60px;
.join-container{
margin-top:48px;
}
.user {
margin-bottom: 85px;
.image {
......
/*
* @Author: 吴文洁
* @Date: 2019-09-10 18:26:03
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-05-10 10:34:13
* @LastEditors: Please set LastEditors
* @LastEditTime: 2021-05-12 17:30:25
* @Description:
*/
import React, { useRef, useContext, useEffect, useState } from "react";
......@@ -183,7 +183,7 @@ function Header(props) {
}
function handleLogout() {
BaseService.logout({}).then((res) => {
BaseService.logout({identifier:User.getIdentifier()}).then((res) => {
User.removeUserId();
User.removeToken();
User.removeEnterpriseId();
......@@ -246,7 +246,7 @@ function Header(props) {
}}
className="college-container"
>
<div>
<div style={{ width: '100%', height: '100%' }}>
<div className="college" onClick={() => setOpenDropdown(false)}>
<span
className="college-name"
......
......@@ -189,6 +189,8 @@
align-items: center;
margin-right: 16px;
cursor: pointer;
width: 100%;
height: 100%;
.select {
cursor: pointer;
}
......
......@@ -14,13 +14,12 @@ function Aside(props: any) {
const ctx: any = useContext(XMContext);
const [selectKey, setSelectKey] = useState();
const [openKeys, setOpenKeys] = useState(['']);
const rootSubmenuKeys = _.pluck(menuList, 'groupCode');
useEffect(() => {
const link = props.location.pathname;
console.log('link',link);
menuList.map((item: any, index: any) => {
if (link.indexOf(item.link) !== -1) {
setSelectKey(item.groupCode);
console.log('selectKey',selectKey);
setOpenKeys([])
} else if (item.children) {
item.children.map((_item: any, _index: any) => {
......@@ -35,9 +34,14 @@ function Aside(props: any) {
function toggleMenu(item: any) {
window.RCHistory.push(item.link)
}
function onOpenChange(openKeys:any){
setOpenKeys(_.last(openKeys, 1))
function onOpenChange(key: any) {
if (openKeys.includes(key)) {
setOpenKeys([]);
} else {
setOpenKeys([key]);
}
}
return (
<div
id="left-container"
......@@ -52,10 +56,8 @@ function Aside(props: any) {
<Menu
style={{ minHeight: "100%", background: '#0E1935' }}
defaultSelectedKeys={selectKey}
selectedKeys={selectKey}
openKeys={openKeys}
onOpenChange={onOpenChange}
theme="dark"
inlineCollapsed={false}
mode={menuType ? "inline" : "vertical"}
......@@ -66,12 +68,24 @@ function Aside(props: any) {
return null;
}
if (item.children) {
return <SubMenu key={item.groupCode} style={{ marginTop: 0 }} className="first-menu-item" title={<div >
<span style={{ marginRight: 6 }} className="iconfont icon" dangerouslySetInnerHTML={{ __html:item.icon}}></span>
return <SubMenu
key={item.groupCode}
style={{ marginTop: 0 }}
className="first-menu-item"
title={
<div>
<span
style={{ marginRight: 6 }}
className="iconfont icon"
dangerouslySetInnerHTML={{ __html: item.icon }}
></span>
{menuType &&
<span>{item.groupName}</span>
}</div>
}>
}
</div>
}
onTitleClick={() => onOpenChange(item.groupCode)}
>
{
item.children.map((_item: any, _index: any) => {
if (ctx.xmState.storeUserPermissionList.indexOf(_item.groupCode) === -1) {
......
......@@ -19,14 +19,14 @@
margin-top: 24px;
.error{
position: absolute;
width: 100%;
height: 100%;
width: 170px;
height: 170px;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items:center;
justify-content:center;
left:0px;
top: 0px;
left:-5px;
top:-5px;
div{
margin: 0 10px;
font-size: 14px;
......
......@@ -77,6 +77,7 @@ export default function WechatLogin(props: any) {
User.setUserId(_res.result.loginInfo.userId);
User.setToken(_res.result.loginInfo.xmToken);
User.setEnterpriseId(_res.result.enterpriseId);
User.setIdentifier(_res.result.identifier)
window.RCHistory.push({
pathname: `/switch-route`,
})
......
......@@ -298,7 +298,7 @@ function AddEmployeeModal(props: AddEmployeeModalProps) {
<Radio value={"CloudOperator"} className="mt-4">
<span style={{ color: "#333" }}>运营师</span>
<p className="radio-tip">
仅可查看/转发培训计划内容,并查看其负责的用户学习进度
仅可查看/转发培训计划内容,并查看其负责的学员学习进度
</p>
</Radio>
</RadioGroup>
......
/*
* @Author: wufan
* @Date: 2020-11-30 10:47:38
* @LastEditors: zhangleyuan
* @LastEditTime: 2021-02-02 10:22:34
* @Description: 用户管理页面
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:38:40
* @Description: 学员管理页面
* @@Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -49,7 +49,7 @@ function UserManagePage() {
function parseColumn() {
return [
{
title: "用户姓名",
title: "学员姓名",
dataIndex: "nickName",
render: (val: string) => {
return (
......@@ -81,7 +81,7 @@ function UserManagePage() {
return (
<div className=" page user-manage-page">
<div className="content-header">用户管理</div>
<div className="content-header">学员管理</div>
<div className="box">
<div className="box-header">
<div
......@@ -93,13 +93,13 @@ function UserManagePage() {
}}
>
<div>
搜索用户
搜索学员
<Search
style={{
width: 300,
marginRight: 40,
}}
placeholder="搜索用户姓名/手机号"
placeholder="搜索学员姓名/手机号"
onSearch={(value) => {
const _query = { ...query };
if (value) {
......
......@@ -365,7 +365,7 @@ function AddExam(props: any) {
checked={needPhone == 'NEED_PHONE_VERIFY'}
onChange={(val) => { setNeedPhone(val ? 'NEED_PHONE_VERIFY' : 'DO_NOT_NEED_PHONE_VERIFY') }}
></Switch>
<div style={{ position: 'relative', top: 3, left: 8, color: "#999" }}><p>开启:需要绑定手机号的用户才能参加考试</p>
<div style={{ position: 'relative', top: 3, left: 8, color: "#999" }}><p>开启:需要绑定手机号的学员才能参加考试</p>
<p>关闭:微信/企业微信登陆直接参加考试</p></div>
</div>
......
......@@ -52,6 +52,10 @@ class ExamShareModal extends React.Component {
const dom = document.querySelector('#poster');
html2canvas(dom, {
useCORS: true,
scale: 2,
logging: false,
background: '#FFF',
allowTaint: true,
}).then(canvas => {
const downloadDOM = document.createElement('a');
const { courseName } = this.props.data;
......@@ -118,25 +122,11 @@ class ExamShareModal extends React.Component {
</div>
<div className="right">
<div className="share-poster right__item">
<div className="title">① 海报分享</div>
<div className="sub-title">用户可通过微信扫描海报二维码,查看考试</div>
<div className="sub-title">学员可通过微信扫描海报二维码,查看考试</div>
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
</div>
{/* <div className="share-url right__item" style={{ marginTop: 40 }}>
<div className="title">② 链接分享</div>
<div className="sub-title">用户可通过微信或浏览器打开以下链接,查看考试(建议使用谷歌浏览器)</div>
<div className="content">
<div className="share-url" id="shareUrl">{shareUrl}</div>
<Button
type="primary"
onClick={this.handleCopy}
>复制</Button>
</div>
</div> */}
</div>
</Modal>
)
......
......@@ -115,7 +115,7 @@ function DataAnalysic(props: any) {
const columns = [
{
title: "用户",
title: "学员",
dataIndex: "userName",
render: (text: any, record: any) => <span>{text}<span style={{ color: record.userSource === 'WORK_WE_CHAT' ? 'rgba(255, 157, 20, 1)' : 'rgba(29, 204, 101, 1)' }} >@{(userTypeEnum as any)[record.userSource]}</span></span>,
},
......@@ -221,11 +221,11 @@ function DataAnalysic(props: any) {
<div style={{ display: 'flex' }}>
<div className="search-condition">
<div className="search-condition__item">
<span className="search-name">用户:</span>
<span className="search-name">学员:</span>
<Search
value={query.examName}
className='search-input'
placeholder="搜索用户名或手机号"
placeholder="搜索学员名或手机号"
onChange={(e) => {
const _query = { ...query }
_query.searchKey = e.target.value
......@@ -237,8 +237,8 @@ function DataAnalysic(props: any) {
</div>
<div className="search-condition__item">
<span className="search-name">用户类型:</span>
<Select value={query.userSource} placeholder="请选择用户类型" onChange={(val) => {
<span className="search-name">学员类型:</span>
<Select value={query.userSource} placeholder="请选择学员类型" onChange={(val) => {
const _query = { ...query }
_query.userSource = val
setQuery(_query);
......
......@@ -2,7 +2,7 @@
* @Author: yuananting
* @Date: 2021-02-25 11:23:47
* @LastEditors: yuananting
* @LastEditTime: 2021-04-15 11:04:03
* @LastEditTime: 2021-05-20 11:37:40
* @Description: 助学工具-题库-试卷列表数据
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -441,7 +441,6 @@ class PaperList extends Component {
pagination={false}
bordered
loading={loading}
onChange={this.handleChangeTable}
/>
) : (
<Table
......@@ -455,7 +454,6 @@ class PaperList extends Component {
columns={this.parseColumns()}
pagination={false}
bordered
onChange={this.handleChangeTable}
/>
)}
</ConfigProvider>
......
......@@ -2,7 +2,7 @@
* @Author: yuananting
* @Date: 2021-02-25 11:23:47
* @LastEditors: yuananting
* @LastEditTime: 2021-04-13 13:56:05
* @LastEditTime: 2021-05-20 11:43:21
* @Description: 助学工具-新建试卷-选择题目列表
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -108,15 +108,14 @@ class SelectQuestionList extends Component {
handleChangeTable = (pagination, filters, sorter) => {
const { columnKey, order } = sorter;
let sort = "UPDATED_DESC";
if (order === "ascend") {
if (columnKey === "accuracy" && order === "ascend") {
sort = "ACCURACY_ASC";
}
if (order === "descend") {
if (columnKey === "accuracy" && order === "descend") {
sort = "ACCURACY_DESC";
}
const _query = this.state.query;
_query.order = sort;
_query.current = 1;
this.setState({ query: _query }, () => this.queryQuestionPageListWithContent());
};
......
......@@ -53,8 +53,14 @@
}
.input-box {
line-height: 20px;
* {
display: inline;
display: inline-block;
vertical-align: top;
*:not(p) {
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
.add-fill-line {
padding: 0 10px;
......
......@@ -120,7 +120,7 @@ class QuestionEditor extends Component {
};
editorRoot.config.zIndex = 999;
editorRoot.config.placeholder = "";
editorRoot.config.pasteFilterStyle = false;
editorRoot.config.pasteFilterStyle = true;
editorRoot.config.pasteIgnoreImg = true;
editorRoot.config.focus = false;
// 自定义处理粘贴的文本内容
......@@ -133,10 +133,11 @@ class QuestionEditor extends Component {
str1 = str1.replace(/[ | ]*\n/g, "\n");
str1 = str1.replace(/\&nbsp\;/gi, " ");
str1 = str1.replace(/[\r\n]/g, "");
str1 = str1.replace(/<\/?a.*?>/g, "");
var str2 = content; // 保留空格和换行的其他字符
str2 = str2.replace(/<xml>[\s\S]*?<\/xml>/gi, "");
str2 = str2.replace(/<style>[\s\S]*?<\/style>/gi, "");
str2 = str2.replace(/<(?!br).*?>/g, "");
str2 = str2.replace(/<\/?a.*?>/g, "");
if (editorRoot.txt.text().length + str1.length > 1000) {
content = str2.substring(0, 1000);
message.error("内容过长,不能超过1000字");
......
......@@ -38,10 +38,18 @@
border: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
font-size: 14px !important;
.editor-box_content {
width: calc(100% - 80px);
p {
display: inline;
font-size: 14px !important;
}
*:not(p){
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
}
.editor-limit {
......@@ -56,13 +64,21 @@
border-radius: 4px;
padding: 4px 0;
border: 1px solid #e8e8e8;
font-size: 14px !important;
.editor-box_content {
max-height: 110px;
overflow: auto;
p {
display: inline;
font-size: 14px !important;
overflow-y: scroll;
}
*:not(p) {
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
}
.editor-limit {
text-align: right;
......
......@@ -2,7 +2,7 @@
* @Author: yuananting
* @Date: 2021-02-25 11:23:47
* @LastEditors: yuananting
* @LastEditTime: 2021-04-12 14:14:19
* @LastEditTime: 2021-05-20 11:35:07
* @Description: 助学工具-题库-题目列表数据
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -161,7 +161,7 @@ class QuestionList extends Component {
height: 100,
}}
description={
<div>
<span>
<span>还没有题目</span>
{["CloudManager", "StoreManager"].includes(User.getUserRole()) &&
categoryId && (
......@@ -178,7 +178,7 @@ class QuestionList extends Component {
吧!
</span>
)}
</div>
</span>
}
></Empty>
);
......@@ -202,7 +202,6 @@ class QuestionList extends Component {
}
const _query = this.state.query;
_query.order = sort || "UPDATED_DESC";
_query.current = 1;
this.setState({ query: _query }, () => this.queryQuestionPageList());
};
......@@ -225,16 +224,27 @@ class QuestionList extends Component {
handleVal = handleVal.replace(/<(?!img|input).*?>/g, "");
handleVal = handleVal.replace(/<\s?input[^>]*>/gi, "_、");
handleVal = handleVal.replace(/\&nbsp\;/gi, " ");
handleVal = handleVal.replace(/style\s*?=\s*?([‘"])[\s\S]*?\1/gi, "");
return (
<Tooltip
overlayClassName="aid-tool-list"
title={
<div style={{ maxWidth: 700, width: "auto" }}>{handleVal}</div>
<div
style={{ maxWidth: 700, width: "auto" }}
dangerouslySetInnerHTML={{
__html: handleVal,
}}
/>
}
placement="topLeft"
overlayStyle={{ maxWidth: 700 }}
>
{handleVal}
<div
className="one-line-text"
dangerouslySetInnerHTML={{
__html: handleVal,
}}
/>
</Tooltip>
);
},
......
......@@ -32,8 +32,13 @@
color: #666666;
.input-box {
margin-bottom: 8px;
* {
display: inline-block;
*:not(p) {
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
}
.picture-box {
......@@ -102,6 +107,13 @@
display: inline-block;
max-width: calc(100% - 20px);
vertical-align: top;
*:not(p) {
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
}
}
......@@ -197,8 +209,13 @@
color: #666666;
.desc-input-box {
margin-bottom: 8px;
* {
display: inline-block;
*:not(p) {
font-weight: normal !important;
font-size: 14px !important;
font-style: normal !important;
text-decoration: none !important;
margin: 0 !important;
}
}
.desc-picture-box {
......
/*
* @Author: 吴文洁
* @Date: 2020-04-29 10:26:32
* @LastEditors: yuananting
* @LastEditTime: 2021-04-15 21:45:42
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:39:51
* @Description: 内容线路由配置
*/
import Home from '@/modules/home/Home';
......@@ -17,8 +17,10 @@ import LiveCoursePage from '@/modules/course-manage/LiveCoursePage';
import AddLivePage from '@/modules/course-manage/AddLive'
import VideoCoursePage from '@/modules/course-manage/video-course'
import GraphicsCoursePage from '@/modules/course-manage/graphics-course'
import OfflineCoursePage from '@/modules/course-manage/offline-course'
import AddVideoCoursePage from '@/modules/course-manage/video-course/AddVideoCourse'
import AddGraphicsCoursePage from '@/modules/course-manage/graphics-course/AddGraphicsCourse'
import AddOfflineCoursePage from '@/modules/course-manage/offline-course/AddOfflineCourse'
// import DataList from '@/modules/course-manage/DataList/DataList';
// import ClassBook from '@/modules/resource-disk';
import ResourceDisk from '@/modules/resource-disk';
......@@ -58,23 +60,18 @@ const mainRoutes = [
{
path: "/user-manage",
component: UserManagePage,
name: "用户管理",
name: "学员管理",
},
{
path: '/college-user',
component: UserManage,
name: '用户管理'
name: '学员管理'
},
{
path: '/store-decoration',
component: StoreDecorationPage,
name: '学院装修'
},
// {
// path: "/course-catalog",
// component: CourseCatalogPage,
// name: "课程分类",
// },
{
path: "/live-course",
component: LiveCoursePage,
......@@ -91,6 +88,11 @@ const mainRoutes = [
name: "图文课",
},
{
path: "/offline-course",
component: OfflineCoursePage,
name: "线下课",
},
{
path: "/create-live-course",
component: AddLivePage,
name: "创建直播课",
......@@ -112,6 +114,11 @@ const mainRoutes = [
name: "创建图文课",
},
{
path: "/create-offline-course",
component: AddOfflineCoursePage,
name: "创建线下课",
},
{
path: "/resource-disk",
component: ResourceDisk,
name: "资料云盘",
......
/*
* @Author: yuananting
* @Date: 2021-02-21 15:53:31
* @LastEditors: yuananting
* @LastEditTime: 2021-03-27 15:20:42
* @LastEditors: wufan
* @LastEditTime: 2021-05-13 16:40:11
* @Description: 描述一下咯
* @Copyrigh: © 2020 杭州杰竞科技有限公司 版权所有
*/
......@@ -33,6 +33,11 @@ export const menuList: any = [
groupCode: "GraphicLesson",
link: '/graphics-course'
},
{
groupName: "线下课",
groupCode: "OfflineClass",
link: '/offline-course'
},
]
},
{
......@@ -97,7 +102,7 @@ export const menuList: any = [
link: '/college-employee'
},
{
groupName: "用户管理",
groupName: "学员管理",
groupCode: "ShopUser",
link: '/college-user'
},
......
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