Commit dffb2d6d by zhangleyuan

feat:处理课程管理页面

parent c9bfedd3
'use strict';
var extend = require('extend');
var qrcodeAlgObjCache = [];
var QRCodeAlg = require('./qrcodealg');
/**
* 计算矩阵点的前景色
* @param {Obj} config
* @param {Number} config.row 点x坐标
* @param {Number} config.col 点y坐标
* @param {Number} config.count 矩阵大小
* @param {Number} config.options 组件的options
* @return {String}
*/
var getForeGround = function getForeGround(config) {
var options = config.options;
if (options.pdground && (config.row > 1 && config.row < 5 && config.col > 1 && config.col < 5 || config.row > config.count - 6 && config.row < config.count - 2 && config.col > 1 && config.col < 5 || config.row > 1 && config.row < 5 && config.col > config.count - 6 && config.col < config.count - 2)) {
return options.pdground;
}
return options.foreground;
};
/**
* 点是否在Position Detection
* @param {row} 矩阵行
* @param {col} 矩阵列
* @param {count} 矩阵大小
* @return {Boolean}
*/
var inPositionDetection = function inPositionDetection(row, col, count) {
if (row < 7 && col < 7 || row > count - 8 && col < 7 || row < 7 && col > count - 8) {
return true;
}
return false;
};
/**
* 二维码构造函数,主要用于绘制
* @param {参数列表} opt 传递参数
* @return {}
*/
var qrcode = function qrcode(opt) {
if (typeof opt === 'string') {
// 只编码ASCII字符串
opt = {
text: opt
};
}
//设置默认参数
this.options = extend({}, {
text: '',
render: '',
size: 256,
correctLevel: 3,
background: '#ffffff',
foreground: '#000000',
image: '',
imageSize: 30
}, opt);
//使用QRCodeAlg创建二维码结构
var qrCodeAlg = null;
for (var i = 0, l = qrcodeAlgObjCache.length; i < l; i++) {
if (qrcodeAlgObjCache[i].text == this.options.text && qrcodeAlgObjCache[i].text.correctLevel == this.options.correctLevel) {
qrCodeAlg = qrcodeAlgObjCache[i].obj;
break;
}
}
if (i == l) {
qrCodeAlg = new QRCodeAlg(this.options.text, this.options.correctLevel);
qrcodeAlgObjCache.push({ text: this.options.text, correctLevel: this.options.correctLevel, obj: qrCodeAlg });
}
if (this.options.render) {
switch (this.options.render) {
case 'canvas':
return this.createCanvas(qrCodeAlg);
case 'table':
return this.createTable(qrCodeAlg);
case 'svg':
return this.createSVG(qrCodeAlg);
default:
return this.createDefault(qrCodeAlg);
}
}
return this.createDefault(qrCodeAlg);
};
extend(qrcode.prototype, {
// default create canvas -> svg -> table
createDefault: function createDefault(qrCodeAlg) {
var canvas = document.createElement('canvas');
if (canvas.getContext) {
return this.createCanvas(qrCodeAlg);
}
var SVG_NS = 'http://www.w3.org/2000/svg';
if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
return this.createSVG(qrCodeAlg);
}
return this.createTable(qrCodeAlg);
},
// canvas create
createCanvas: function createCanvas(qrCodeAlg) {
var options = this.options;
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var count = qrCodeAlg.getModuleCount();
// preload img
var loadImage = function loadImage(url, callback) {
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
img.onload = function () {
callback(this);
img.onload = null;
};
};
//计算每个点的长宽
var tileW = (options.size / count).toPrecision(4);
var tileH = (options.size / count).toPrecision(4);
canvas.width = options.size;
canvas.height = options.size;
//绘制
for (var row = 0; row < count; row++) {
for (var col = 0; col < count; col++) {
var w = Math.ceil((col + 1) * tileW) - Math.floor(col * tileW);
var h = Math.ceil((row + 1) * tileW) - Math.floor(row * tileW);
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
ctx.fillStyle = qrCodeAlg.modules[row][col] ? foreground : options.background;
ctx.fillRect(Math.round(col * tileW), Math.round(row * tileH), w, h);
}
}
if (options.image) {
loadImage(options.image, function (img) {
var x = ((options.size - options.imageSize) / 2).toFixed(2);
var y = ((options.size - options.imageSize) / 2).toFixed(2);
ctx.drawImage(img, x, y, options.imageSize, options.imageSize);
});
}
return canvas;
},
// table create
createTable: function createTable(qrCodeAlg) {
var options = this.options;
var count = qrCodeAlg.getModuleCount();
// 计算每个节点的长宽;取整,防止点之间出现分离
var tileW = Math.floor(options.size / count);
var tileH = Math.floor(options.size / count);
if (tileW <= 0) {
tileW = count < 80 ? 2 : 1;
}
if (tileH <= 0) {
tileH = count < 80 ? 2 : 1;
}
//创建table节点
//重算码大小
var s = [];
s.push('<table style="border:0px; margin:0px; padding:0px; border-collapse:collapse; background-color:' + options.background + ';">');
// 绘制二维码
for (var row = 0; row < count; row++) {
s.push('<tr style="border:0px; margin:0px; padding:0px; height:' + tileH + 'px">');
for (var col = 0; col < count; col++) {
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
if (qrCodeAlg.modules[row][col]) {
s.push('<td style="border:0px; margin:0px; padding:0px; width:' + tileW + 'px; background-color:' + foreground + '"></td>');
} else {
s.push('<td style="border:0px; margin:0px; padding:0px; width:' + tileW + 'px; background-color:' + options.background + '"></td>');
}
}
s.push('</tr>');
}
s.push('</table>');
if (options.image) {
// 计算表格的总大小
var width = tileW * count;
var height = tileH * count;
var x = ((width - options.imageSize) / 2).toFixed(2);
var y = ((height - options.imageSize) / 2).toFixed(2);
s.unshift('<div style=\'position:relative; \n width:' + width + 'px; \n height:' + height + 'px;\'>');
s.push('<img src=\'' + options.image + '\' \n width=\'' + options.imageSize + '\' \n height=\'' + options.imageSize + '\' \n style=\'position:absolute;left:' + x + 'px; top:' + y + 'px;\'>');
s.push('</div>');
}
var span = document.createElement('span');
span.innerHTML = s.join('');
return span.firstChild;
},
// create svg
createSVG: function createSVG(qrCodeAlg) {
var options = this.options;
var count = qrCodeAlg.getModuleCount();
var scale = count / options.size;
// create svg
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', options.size);
svg.setAttribute('height', options.size);
svg.setAttribute('viewBox', '0 0 ' + count + ' ' + count);
for (var row = 0; row < count; row++) {
for (var col = 0; col < count; col++) {
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
var foreground = getForeGround({
row: row,
col: col,
count: count,
options: options
});
rect.setAttribute('x', col);
rect.setAttribute('y', row);
rect.setAttribute('width', 1);
rect.setAttribute('height', 1);
rect.setAttribute('stroke-width', 0);
if (qrCodeAlg.modules[row][col]) {
rect.setAttribute('fill', foreground);
} else {
rect.setAttribute('fill', options.background);
}
svg.appendChild(rect);
}
}
// create image
if (options.image) {
var img = document.createElementNS('http://www.w3.org/2000/svg', 'image');
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', options.image);
img.setAttribute('x', ((count - options.imageSize * scale) / 2).toFixed(2));
img.setAttribute('y', ((count - options.imageSize * scale) / 2).toFixed(2));
img.setAttribute('width', options.imageSize * scale);
img.setAttribute('height', options.imageSize * scale);
svg.appendChild(img);
}
return svg;
}
});
module.exports = qrcode;
\ No newline at end of file
/**
* 获取单个字符的utf8编码
* unicode BMP平面约65535个字符
* @param {num} code
* return {array}
*/
"use strict";
function unicodeFormat8(code) {
// 1 byte
if (code < 128) {
return [code];
// 2 bytes
} else if (code < 2048) {
c0 = 192 + (code >> 6);
c1 = 128 + (code & 63);
return [c0, c1];
// 3 bytes
} else {
c0 = 224 + (code >> 12);
c1 = 128 + (code >> 6 & 63);
c2 = 128 + (code & 63);
return [c0, c1, c2];
}
}
/**
* 获取字符串的utf8编码字节串
* @param {string} string
* @return {array}
*/
function getUTF8Bytes(string) {
var utf8codes = [];
for (var i = 0; i < string.length; i++) {
var code = string.charCodeAt(i);
var utf8 = unicodeFormat8(code);
for (var j = 0; j < utf8.length; j++) {
utf8codes.push(utf8[j]);
}
}
return utf8codes;
}
/**
* 二维码算法实现
* @param {string} data 要编码的信息字符串
* @param {num} errorCorrectLevel 纠错等级
*/
function QRCodeAlg(data, errorCorrectLevel) {
this.typeNumber = -1; //版本
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null; //二维矩阵,存放最终结果
this.moduleCount = 0; //矩阵大小
this.dataCache = null; //数据缓存
this.rsBlocks = null; //版本数据信息
this.totalDataCount = -1; //可使用的数据量
this.data = data;
this.utf8bytes = getUTF8Bytes(data);
this.make();
}
QRCodeAlg.prototype = {
constructor: QRCodeAlg,
/**
* 获取二维码矩阵大小
* @return {num} 矩阵大小
*/
getModuleCount: function getModuleCount() {
return this.moduleCount;
},
/**
* 编码
*/
make: function make() {
this.getRightType();
this.dataCache = this.createData();
this.createQrcode();
},
/**
* 设置二位矩阵功能图形
* @param {bool} test 表示是否在寻找最好掩膜阶段
* @param {num} maskPattern 掩膜的版本
*/
makeImpl: function makeImpl(maskPattern) {
this.moduleCount = this.typeNumber * 4 + 17;
this.modules = new Array(this.moduleCount);
for (var row = 0; row < this.moduleCount; row++) {
this.modules[row] = new Array(this.moduleCount);
}
this.setupPositionProbePattern(0, 0);
this.setupPositionProbePattern(this.moduleCount - 7, 0);
this.setupPositionProbePattern(0, this.moduleCount - 7);
this.setupPositionAdjustPattern();
this.setupTimingPattern();
this.setupTypeInfo(true, maskPattern);
if (this.typeNumber >= 7) {
this.setupTypeNumber(true);
}
this.mapData(this.dataCache, maskPattern);
},
/**
* 设置二维码的位置探测图形
* @param {num} row 探测图形的中心横坐标
* @param {num} col 探测图形的中心纵坐标
*/
setupPositionProbePattern: function setupPositionProbePattern(row, col) {
for (var r = -1; r <= 7; r++) {
if (row + r <= -1 || this.moduleCount <= row + r) continue;
for (var c = -1; c <= 7; c++) {
if (col + c <= -1 || this.moduleCount <= col + c) continue;
if (0 <= r && r <= 6 && (c == 0 || c == 6) || 0 <= c && c <= 6 && (r == 0 || r == 6) || 2 <= r && r <= 4 && 2 <= c && c <= 4) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
},
/**
* 创建二维码
* @return {[type]} [description]
*/
createQrcode: function createQrcode() {
var minLostPoint = 0;
var pattern = 0;
var bestModules = null;
for (var i = 0; i < 8; i++) {
this.makeImpl(i);
var lostPoint = QRUtil.getLostPoint(this);
if (i == 0 || minLostPoint > lostPoint) {
minLostPoint = lostPoint;
pattern = i;
bestModules = this.modules;
}
}
this.modules = bestModules;
this.setupTypeInfo(false, pattern);
if (this.typeNumber >= 7) {
this.setupTypeNumber(false);
}
},
/**
* 设置定位图形
* @return {[type]} [description]
*/
setupTimingPattern: function setupTimingPattern() {
for (var r = 8; r < this.moduleCount - 8; r++) {
if (this.modules[r][6] != null) {
continue;
}
this.modules[r][6] = r % 2 == 0;
if (this.modules[6][r] != null) {
continue;
}
this.modules[6][r] = r % 2 == 0;
}
},
/**
* 设置矫正图形
* @return {[type]} [description]
*/
setupPositionAdjustPattern: function setupPositionAdjustPattern() {
var pos = QRUtil.getPatternPosition(this.typeNumber);
for (var i = 0; i < pos.length; i++) {
for (var j = 0; j < pos.length; j++) {
var row = pos[i];
var col = pos[j];
if (this.modules[row][col] != null) {
continue;
}
for (var r = -2; r <= 2; r++) {
for (var c = -2; c <= 2; c++) {
if (r == -2 || r == 2 || c == -2 || c == 2 || r == 0 && c == 0) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
}
},
/**
* 设置版本信息(7以上版本才有)
* @param {bool} test 是否处于判断最佳掩膜阶段
* @return {[type]} [description]
*/
setupTypeNumber: function setupTypeNumber(test) {
var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
for (var i = 0; i < 18; i++) {
var mod = !test && (bits >> i & 1) == 1;
this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod;
this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
}
},
/**
* 设置格式信息(纠错等级和掩膜版本)
* @param {bool} test
* @param {num} maskPattern 掩膜版本
* @return {}
*/
setupTypeInfo: function setupTypeInfo(test, maskPattern) {
var data = QRErrorCorrectLevel[this.errorCorrectLevel] << 3 | maskPattern;
var bits = QRUtil.getBCHTypeInfo(data);
// vertical
for (var i = 0; i < 15; i++) {
var mod = !test && (bits >> i & 1) == 1;
if (i < 6) {
this.modules[i][8] = mod;
} else if (i < 8) {
this.modules[i + 1][8] = mod;
} else {
this.modules[this.moduleCount - 15 + i][8] = mod;
}
// horizontal
var mod = !test && (bits >> i & 1) == 1;
if (i < 8) {
this.modules[8][this.moduleCount - i - 1] = mod;
} else if (i < 9) {
this.modules[8][15 - i - 1 + 1] = mod;
} else {
this.modules[8][15 - i - 1] = mod;
}
}
// fixed module
this.modules[this.moduleCount - 8][8] = !test;
},
/**
* 数据编码
* @return {[type]} [description]
*/
createData: function createData() {
var buffer = new QRBitBuffer();
var lengthBits = this.typeNumber > 9 ? 16 : 8;
buffer.put(4, 4); //添加模式
buffer.put(this.utf8bytes.length, lengthBits);
for (var i = 0, l = this.utf8bytes.length; i < l; i++) {
buffer.put(this.utf8bytes[i], 8);
}
if (buffer.length + 4 <= this.totalDataCount * 8) {
buffer.put(0, 4);
}
// padding
while (buffer.length % 8 != 0) {
buffer.putBit(false);
}
// padding
while (true) {
if (buffer.length >= this.totalDataCount * 8) {
break;
}
buffer.put(QRCodeAlg.PAD0, 8);
if (buffer.length >= this.totalDataCount * 8) {
break;
}
buffer.put(QRCodeAlg.PAD1, 8);
}
return this.createBytes(buffer);
},
/**
* 纠错码编码
* @param {buffer} buffer 数据编码
* @return {[type]}
*/
createBytes: function createBytes(buffer) {
var offset = 0;
var maxDcCount = 0;
var maxEcCount = 0;
var length = this.rsBlock.length / 3;
var rsBlocks = new Array();
for (var i = 0; i < length; i++) {
var count = this.rsBlock[i * 3 + 0];
var totalCount = this.rsBlock[i * 3 + 1];
var dataCount = this.rsBlock[i * 3 + 2];
for (var j = 0; j < count; j++) {
rsBlocks.push([dataCount, totalCount]);
}
}
var dcdata = new Array(rsBlocks.length);
var ecdata = new Array(rsBlocks.length);
for (var r = 0; r < rsBlocks.length; r++) {
var dcCount = rsBlocks[r][0];
var ecCount = rsBlocks[r][1] - dcCount;
maxDcCount = Math.max(maxDcCount, dcCount);
maxEcCount = Math.max(maxEcCount, ecCount);
dcdata[r] = new Array(dcCount);
for (var i = 0; i < dcdata[r].length; i++) {
dcdata[r][i] = 0xff & buffer.buffer[i + offset];
}
offset += dcCount;
var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
var modPoly = rawPoly.mod(rsPoly);
ecdata[r] = new Array(rsPoly.getLength() - 1);
for (var i = 0; i < ecdata[r].length; i++) {
var modIndex = i + modPoly.getLength() - ecdata[r].length;
ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0;
}
}
var data = new Array(this.totalDataCount);
var index = 0;
for (var i = 0; i < maxDcCount; i++) {
for (var r = 0; r < rsBlocks.length; r++) {
if (i < dcdata[r].length) {
data[index++] = dcdata[r][i];
}
}
}
for (var i = 0; i < maxEcCount; i++) {
for (var r = 0; r < rsBlocks.length; r++) {
if (i < ecdata[r].length) {
data[index++] = ecdata[r][i];
}
}
}
return data;
},
/**
* 布置模块,构建最终信息
* @param {} data
* @param {} maskPattern
* @return {}
*/
mapData: function mapData(data, maskPattern) {
var inc = -1;
var row = this.moduleCount - 1;
var bitIndex = 7;
var byteIndex = 0;
for (var col = this.moduleCount - 1; col > 0; col -= 2) {
if (col == 6) col--;
while (true) {
for (var c = 0; c < 2; c++) {
if (this.modules[row][col - c] == null) {
var dark = false;
if (byteIndex < data.length) {
dark = (data[byteIndex] >>> bitIndex & 1) == 1;
}
var mask = QRUtil.getMask(maskPattern, row, col - c);
if (mask) {
dark = !dark;
}
this.modules[row][col - c] = dark;
bitIndex--;
if (bitIndex == -1) {
byteIndex++;
bitIndex = 7;
}
}
}
row += inc;
if (row < 0 || this.moduleCount <= row) {
row -= inc;
inc = -inc;
break;
}
}
}
}
};
/**
* 填充字段
*/
QRCodeAlg.PAD0 = 0xEC;
QRCodeAlg.PAD1 = 0x11;
//---------------------------------------------------------------------
// 纠错等级对应的编码
//---------------------------------------------------------------------
var QRErrorCorrectLevel = [1, 0, 3, 2];
//---------------------------------------------------------------------
// 掩膜版本
//---------------------------------------------------------------------
var QRMaskPattern = {
PATTERN000: 0,
PATTERN001: 1,
PATTERN010: 2,
PATTERN011: 3,
PATTERN100: 4,
PATTERN101: 5,
PATTERN110: 6,
PATTERN111: 7
};
//---------------------------------------------------------------------
// 工具类
//---------------------------------------------------------------------
var QRUtil = {
/*
每个版本矫正图形的位置
*/
PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]],
G15: 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0,
G18: 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0,
G15_MASK: 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1,
/*
BCH编码格式信息
*/
getBCHTypeInfo: function getBCHTypeInfo(data) {
var d = data << 10;
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
d ^= QRUtil.G15 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15);
}
return (data << 10 | d) ^ QRUtil.G15_MASK;
},
/*
BCH编码版本信息
*/
getBCHTypeNumber: function getBCHTypeNumber(data) {
var d = data << 12;
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
d ^= QRUtil.G18 << QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18);
}
return data << 12 | d;
},
/*
获取BCH位信息
*/
getBCHDigit: function getBCHDigit(data) {
var digit = 0;
while (data != 0) {
digit++;
data >>>= 1;
}
return digit;
},
/*
获取版本对应的矫正图形位置
*/
getPatternPosition: function getPatternPosition(typeNumber) {
return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1];
},
/*
掩膜算法
*/
getMask: function getMask(maskPattern, i, j) {
switch (maskPattern) {
case QRMaskPattern.PATTERN000:
return (i + j) % 2 == 0;
case QRMaskPattern.PATTERN001:
return i % 2 == 0;
case QRMaskPattern.PATTERN010:
return j % 3 == 0;
case QRMaskPattern.PATTERN011:
return (i + j) % 3 == 0;
case QRMaskPattern.PATTERN100:
return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0;
case QRMaskPattern.PATTERN101:
return i * j % 2 + i * j % 3 == 0;
case QRMaskPattern.PATTERN110:
return (i * j % 2 + i * j % 3) % 2 == 0;
case QRMaskPattern.PATTERN111:
return (i * j % 3 + (i + j) % 2) % 2 == 0;
default:
throw new Error("bad maskPattern:" + maskPattern);
}
},
/*
获取RS的纠错多项式
*/
getErrorCorrectPolynomial: function getErrorCorrectPolynomial(errorCorrectLength) {
var a = new QRPolynomial([1], 0);
for (var i = 0; i < errorCorrectLength; i++) {
a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0));
}
return a;
},
/*
获取评价
*/
getLostPoint: function getLostPoint(qrCode) {
var moduleCount = qrCode.getModuleCount(),
lostPoint = 0,
darkCount = 0;
for (var row = 0; row < moduleCount; row++) {
var sameCount = 0;
var head = qrCode.modules[row][0];
for (var col = 0; col < moduleCount; col++) {
var current = qrCode.modules[row][col];
//level 3 评价
if (col < moduleCount - 6) {
if (current && !qrCode.modules[row][col + 1] && qrCode.modules[row][col + 2] && qrCode.modules[row][col + 3] && qrCode.modules[row][col + 4] && !qrCode.modules[row][col + 5] && qrCode.modules[row][col + 6]) {
if (col < moduleCount - 10) {
if (qrCode.modules[row][col + 7] && qrCode.modules[row][col + 8] && qrCode.modules[row][col + 9] && qrCode.modules[row][col + 10]) {
lostPoint += 40;
}
} else if (col > 3) {
if (qrCode.modules[row][col - 1] && qrCode.modules[row][col - 2] && qrCode.modules[row][col - 3] && qrCode.modules[row][col - 4]) {
lostPoint += 40;
}
}
}
}
//level 2 评价
if (row < moduleCount - 1 && col < moduleCount - 1) {
var count = 0;
if (current) count++;
if (qrCode.modules[row + 1][col]) count++;
if (qrCode.modules[row][col + 1]) count++;
if (qrCode.modules[row + 1][col + 1]) count++;
if (count == 0 || count == 4) {
lostPoint += 3;
}
}
//level 1 评价
if (head ^ current) {
sameCount++;
} else {
head = current;
if (sameCount >= 5) {
lostPoint += 3 + sameCount - 5;
}
sameCount = 1;
}
//level 4 评价
if (current) {
darkCount++;
}
}
}
for (var col = 0; col < moduleCount; col++) {
var sameCount = 0;
var head = qrCode.modules[0][col];
for (var row = 0; row < moduleCount; row++) {
var current = qrCode.modules[row][col];
//level 3 评价
if (row < moduleCount - 6) {
if (current && !qrCode.modules[row + 1][col] && qrCode.modules[row + 2][col] && qrCode.modules[row + 3][col] && qrCode.modules[row + 4][col] && !qrCode.modules[row + 5][col] && qrCode.modules[row + 6][col]) {
if (row < moduleCount - 10) {
if (qrCode.modules[row + 7][col] && qrCode.modules[row + 8][col] && qrCode.modules[row + 9][col] && qrCode.modules[row + 10][col]) {
lostPoint += 40;
}
} else if (row > 3) {
if (qrCode.modules[row - 1][col] && qrCode.modules[row - 2][col] && qrCode.modules[row - 3][col] && qrCode.modules[row - 4][col]) {
lostPoint += 40;
}
}
}
}
//level 1 评价
if (head ^ current) {
sameCount++;
} else {
head = current;
if (sameCount >= 5) {
lostPoint += 3 + sameCount - 5;
}
sameCount = 1;
}
}
}
// LEVEL4
var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
lostPoint += ratio * 10;
return lostPoint;
}
};
//---------------------------------------------------------------------
// QRMath使用的数学工具
//---------------------------------------------------------------------
var QRMath = {
/*
将n转化为a^m
*/
glog: function glog(n) {
if (n < 1) {
throw new Error("glog(" + n + ")");
}
return QRMath.LOG_TABLE[n];
},
/*
将a^m转化为n
*/
gexp: function gexp(n) {
while (n < 0) {
n += 255;
}
while (n >= 256) {
n -= 255;
}
return QRMath.EXP_TABLE[n];
},
EXP_TABLE: new Array(256),
LOG_TABLE: new Array(256)
};
for (var i = 0; i < 8; i++) {
QRMath.EXP_TABLE[i] = 1 << i;
}
for (var i = 8; i < 256; i++) {
QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];
}
for (var i = 0; i < 255; i++) {
QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;
}
//---------------------------------------------------------------------
// QRPolynomial 多项式
//---------------------------------------------------------------------
/**
* 多项式类
* @param {Array} num 系数
* @param {num} shift a^shift
*/
function QRPolynomial(num, shift) {
if (num.length == undefined) {
throw new Error(num.length + "/" + shift);
}
var offset = 0;
while (offset < num.length && num[offset] == 0) {
offset++;
}
this.num = new Array(num.length - offset + shift);
for (var i = 0; i < num.length - offset; i++) {
this.num[i] = num[i + offset];
}
}
QRPolynomial.prototype = {
get: function get(index) {
return this.num[index];
},
getLength: function getLength() {
return this.num.length;
},
/**
* 多项式乘法
* @param {QRPolynomial} e 被乘多项式
* @return {[type]} [description]
*/
multiply: function multiply(e) {
var num = new Array(this.getLength() + e.getLength() - 1);
for (var i = 0; i < this.getLength(); i++) {
for (var j = 0; j < e.getLength(); j++) {
num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)));
}
}
return new QRPolynomial(num, 0);
},
/**
* 多项式模运算
* @param {QRPolynomial} e 模多项式
* @return {}
*/
mod: function mod(e) {
var tl = this.getLength(),
el = e.getLength();
if (tl - el < 0) {
return this;
}
var num = new Array(tl);
for (var i = 0; i < tl; i++) {
num[i] = this.get(i);
}
while (num.length >= el) {
var ratio = QRMath.glog(num[0]) - QRMath.glog(e.get(0));
for (var i = 0; i < e.getLength(); i++) {
num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio);
}
while (num[0] == 0) {
num.shift();
}
}
return new QRPolynomial(num, 0);
}
};
//---------------------------------------------------------------------
// RS_BLOCK_TABLE
//---------------------------------------------------------------------
/*
二维码各个版本信息[块数, 每块中的数据块数, 每块中的信息块数]
*/
var RS_BLOCK_TABLE = [
// L
// M
// Q
// H
// 1
[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9],
// 2
[1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16],
// 3
[1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13],
// 4
[1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9],
// 5
[1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12],
// 6
[2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15],
// 7
[2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14],
// 8
[2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15],
// 9
[2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13],
// 10
[2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16],
// 11
[4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13],
// 12
[2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15],
// 13
[4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12],
// 14
[3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13],
// 15
[5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12],
// 16
[5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16],
// 17
[1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15],
// 18
[5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15],
// 19
[3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14],
// 20
[3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16],
// 21
[4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17],
// 22
[2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13],
// 23
[4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16],
// 24
[6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17],
// 25
[8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16],
// 26
[10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17],
// 27
[8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16],
// 28
[3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16],
// 29
[7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16],
// 30
[5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16],
// 31
[13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16],
// 32
[17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16],
// 33
[17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16],
// 34
[13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17],
// 35
[12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16],
// 36
[6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16],
// 37
[17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16],
// 38
[4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16],
// 39
[20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16],
// 40
[19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]];
/**
* 根据数据获取对应版本
* @return {[type]} [description]
*/
QRCodeAlg.prototype.getRightType = function () {
for (var typeNumber = 1; typeNumber < 41; typeNumber++) {
var rsBlock = RS_BLOCK_TABLE[(typeNumber - 1) * 4 + this.errorCorrectLevel];
if (rsBlock == undefined) {
throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + this.errorCorrectLevel);
}
var length = rsBlock.length / 3;
var totalDataCount = 0;
for (var i = 0; i < length; i++) {
var count = rsBlock[i * 3 + 0];
var dataCount = rsBlock[i * 3 + 2];
totalDataCount += dataCount * count;
}
var lengthBytes = typeNumber > 9 ? 2 : 1;
if (this.utf8bytes.length + lengthBytes < totalDataCount || typeNumber == 40) {
this.typeNumber = typeNumber;
this.rsBlock = rsBlock;
this.totalDataCount = totalDataCount;
break;
}
}
};
//---------------------------------------------------------------------
// QRBitBuffer
//---------------------------------------------------------------------
function QRBitBuffer() {
this.buffer = new Array();
this.length = 0;
}
QRBitBuffer.prototype = {
get: function get(index) {
var bufIndex = Math.floor(index / 8);
return this.buffer[bufIndex] >>> 7 - index % 8 & 1;
},
put: function put(num, length) {
for (var i = 0; i < length; i++) {
this.putBit(num >>> length - i - 1 & 1);
}
},
putBit: function putBit(bit) {
var bufIndex = Math.floor(this.length / 8);
if (this.buffer.length <= bufIndex) {
this.buffer.push(0);
}
if (bit) {
this.buffer[bufIndex] |= 0x80 >>> this.length % 8;
}
this.length++;
}
};
module.exports = QRCodeAlg;
\ No newline at end of file
@import '../../core/variables.less';
.common-select-active {
span {
color: @primary;
}
}
.common-select {
cursor: pointer;
.ant-dropdown-link{
// padding-top: 5px;
}
&.border {
flex: 1;
height: 32px;
line-height: 32px;
border: 1px solid #e8e8e8;
border-radius: 4px;
.ant-dropdown-link {
padding: 0 8px;
width: 100%;
.title {
width: calc(~'100% - 18px');
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
.icon {
float: right;
color: rgba(0, 0, 0, 0.25);
}
}
}
.ant-dropdown-link {
.title {
max-width: 180px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
padding-right: 5px;
}
&:hover {
span {
color: @xm-color-text-select-primary;
}
}
span {
float: left;
line-height: 30px !important;
}
}
.selected-list{
.selected-list-item {
text-align: center;
min-width: 60px;
}
.icon {
position: relative;
.close {
position: absolute;
top: -5px;
right: 5px;
display: none;
}
&:hover {
.close {
display: block;
}
}
}
.icon-tip {
width: 100%;
text-align: center;
position: absolute;
bottom: 9px;
line-height: 10px;
span {
font-size: 8px;
line-height: 10px;
background-color: @primary;
padding: 1px 5px;
color: white;
border-radius: 5px;
}
}
.name {
font-size: 12px;
line-height: 15px;
}
}
}
.common-select-menu {
.header {
.ant-dropdown-link {
margin-left: 10px;
}
}
.footer {
padding-bottom: 0px;
line-height: 30px;
}
.list {
max-height: 250px;
overflow-y: auto;
.student {
padding: 5px 10px;
}
.name {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.student {
.user-name {
padding: 0px 10px;
color: @xm-main-text-color;
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
line-height: 32px;
}
}
.common-select-menu-item {
line-height: 100%;
margin: 10px 0;
.icon {
position: relative;
}
.icon-tip {
width: 100%;
text-align: center;
position: absolute;
bottom: 0px;
line-height: 10px;
span {
font-size: 6px;
line-height: 10px;
background-color: @primary;
padding: 5px;
color: white;
border-radius: 15px;
}
}
}
.common-select-menu-item:hover {
background-color: @xm-select-item-hover;
}
.ant-spin-container:hover {
background-color: white;
}
list:hover {
background-color: white;
}
.class-info {
.name .teacher-name{
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
.teacher-info {
.teacher-name, .subject-name {
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
}
// 下拉框颜色
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.list:hover {
background-color: white !important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item:hover {
background-color: @xm-select-item-hover!important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.header:hover {
background-color: #fff !important;
}
.ant-dropdown-menu-item-active.ant-dropdown-menu-item.footer:hover {
background-color: #fff !important;
}
.ant-dropdown-menu-item:hover {
background-color: white !important;
}
.ant-dropdown-menu-item-selected {
color: white;
}
}
\ No newline at end of file
import React from 'react';
import PropTypes from 'prop-types';
import { DatePicker } from 'antd';
import moment from 'moment';
const { RangePicker } = DatePicker;
class DateRangePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false
}
}
render() {
const showTime = { showTime: true }
return (
<RangePicker
{...this.props}
format={this.props.format || 'YYYY-MM-DD'}
allowClear={this.props.allowClear}
ranges={this.props.ranges || { '本月': [moment().startOf('month'), moment().endOf('month')], '本周': [moment().startOf('week'), moment().endOf('week')], '上月': [moment().subtract(1, 'M').startOf('month'), moment().subtract(1, 'M').endOf('month')], '上周': [moment().subtract(1, 'w').startOf('week'), moment().subtract(1, 'w').endOf('week')] }}
onChange={(date) => {
if (!_.isEmpty(date)) {
date[0] = date[0].startOf('day')
date[1] = date[1].endOf('day')
}
this.props.onChange(date)
}}
{...showTime}
/>
)
}
}
DateRangePicker.propTypes = {
};
DateRangePicker.defaultProps = {
allowClear: true,
}
export default DateRangePicker;
\ No newline at end of file
/*
* @Author: leehu
* @Date: 2017-10-12 16:24:11
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-03-20 10:43:55
*/
import React from 'react';
import { Tooltip, Dropdown, Icon, Checkbox, Button, Input, Spin, Select, Avatar, Row, Col } from 'antd';
import "./CommonSelect.less";
import "./TeacherSearchSelect.less";
import _ from "underscore";
const Search = Input.Search;
const baseImg = require('@/common/images/xiaomai-IMG.png')
class TeacherSearchSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
close: true,
dataSource: [],
query: {
month: null,
current: 1,
size: 10,
nameOrPhone: '',
status: 'ON',
CustomOrderType: 2,
courseId: props.courseId,
// instId: window.currentUserInstInfo.instId,
},
dataSet: [],
visible: false,
loading: false,
selectedIds: _.pluck(props.selected, 'teacherId') || [],
selected: props.selected || [],
isAll: false,
oldDataSet: [],
}
this.reset = () => {
this.handleQueryReset();
}
}
componentWillMount() {
this.fetchServerData();
}
componentWillReceiveProps(nextProps) {
if (this.props.courseId !== nextProps.courseId) {
this.state.query.courseId = nextProps.courseId;
if (!nextProps.courseId) {
delete this.state.query.courseId
}
this.fetchServerData();
}
}
handleQuery = () => {
this.fetchServerData();
}
handleQueryReset = () => {
this.setState({
query: {
current: 1,
size: 10,
nameOrPhone: '',
status: 'ON',
CustomOrderType: 2,
courseId: this.props.courseId,
}
}, () => {
this.fetchServerData();
});
}
searchName = () => {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.fetchServerData()
}, 500)
}
fetchServerData(current = 1) {
this.state.query.current = current;
const self = this;
const url = this.props.url || 'public/teacher/getTeacherDigestPage'
const param = _.extend(this.state.query, this.props.query);
// param.courseId = this.props.courseId || null
this.setState({ fetching: true }, () => {
setTimeout(() => {
this.setState({ fetching: false })
})
})
// axios.Business(url, param).then((res) => {
// let data = this.props.filter ? this.props.filter(res.result.records) : res.result.records;
// if (current > 1) {
// data = this.state.dataSet.concat(data)
// }
// if (this.props.multiple && (current == 1)) {
// _.map(this.props.defaultValue, (item) => {
// const list = _.filter(data, (_item) => {
// return item.teacherId == _item.teacherId
// })
// if (!list.length) {
// data.push(item)
// }
// })
// }
// if (this.props.noTeacher) {
// if (data[0] && data[0].teacherId != '-1') {
// data.unshift(
// { teacherId: '-1', name: '待分配' }
// )
// }
// }
// data = _.uniq(data, (item) => {
// return item.teacherId;
// })
// this.setState({
// dataSet: data,
// totalCount: res.result.total,
// isAll: !res.result.hasNext,
// fetching: false
// });
// }).finally((res) => {
// this.setState({ loading: false });
// });
}
handleTeacherSelect = (teacher) => {
const query = this.state.query;
const { oldDataSet, dataSet } = this.state;
const data = _.find(dataSet, item => item.teacherId == teacher);
if (data) {
oldDataSet.push(data);
const _oldDataSet = _.uniq(oldDataSet, item => item.teacherId);
this.setState({ oldDataSet: _oldDataSet });
}
if (query.name) {
query.name = '';
this.setState({ query }, this.fetchServerData);
}
if (!this.props.multiple) {
const items = _.filter(this.state.dataSet, (_item) => {
return _item.teacherId == teacher;
})
this.props.onSelect({ teacherId: teacher, name: !!items[0] && items[0].name ? items[0].name : '' });
return;
} else {
const list = [];
_.map(teacher, (item) => {
const _list = _.filter(this.state.dataSet, (_item) => {
return _item.teacherId == item;
})
list.push({
name: _list[0].name,
teacherId: item
})
})
this.props.onSelect(list);
}
}
hasmore = (dom) => {
clearTimeout(this.scroll)
let height = $(dom.currentTarget).find('ul li').last().position().top
this.scroll = setTimeout(() => {
if (height < 500 && !this.state.isAll) {
this.fetchServerData(this.state.query.current + 1)
}
}, 300)
}
render() {
let defprops = {}
if (this.props.multiple) {
defprops.mode = 'multiple'
}
let value = [];
value = this.props.defaultValue
const list = _.filter(this.state.dataSet, (item) => {
return item.teacherId == this.props.defaultValue
})
if (!list.length) {
value = this.props.teacherName || undefined
}
if (this.props.multiple) {
value = _.pluck(this.props.defaultValue, 'teacherId')
}
let data = _.filter(this.state.dataSet, item => item.name);
data = _.uniq(data, item => item.teacherId);
return (
<div className={("common-select staticSelect teacher-search-select", { 'common-select-active': this.state.visible })} style={this.props.style}>
{
!!this.props.label && <div className='label'> {this.props.label}:</div>
}
<Select
id={this.props.id}
ref='teacher'
{...defprops}
showSearch
// style={{ width: '100%' }}
allowClear
notFoundContent={this.state.fetching ? <Spin size="small" /> : null}
onSearch={
(value) => {
const query = this.state.query;
query.nameOrPhone = value
this.setState({ query }, this.searchName)
}
}
// open
onPopupScroll={(dom) => {
this.hasmore(dom)
}}
placeholder={this.props.placeholder}
value={value}
onChange={this.handleTeacherSelect}
filterOption={(input, option) => option}
>
{
_.map(data, (item, index) => {
if (this.props.showAvatar) {
return <Select.Option id={'teacher_select_item_' + index} key={item.teacherId} value={item.teacherId} title={item.name}>
<Row align="middle">
<Col span={6}>
<Avatar icon="user" src={item.avatar || baseImg} />
</Col>
<Col span={18}>
<div className="teacher-name" title={item.name}>
{item.name}
</div>
<div className="subject-name" title={item.subjectName || ''}>
{item.subjectName || ''}
</div>
</Col>
</Row>
</Select.Option>
} else {
return <Select.Option id={'teacher_select_item_' + index} key={item.teacherId} value={item.teacherId} title={item.name}>
<Tooltip title={`科目:${item.subjectName || '未设置'}`}>{item.name}</Tooltip>
</Select.Option>
}
})
}
{!this.state.isAll &&
<Select.Option disabled style={{ textAlign: 'center' }} value="spin">
<Spin size="small" />
</Select.Option>
}
</Select>
</div>
)
}
}
TeacherSearchSelect.propTypes = {};
TeacherSearchSelect.defaultProps = {
onSelect: () => { },
name: '选择老师',
courseId: null,
selected: [],
multiple: false,
query: {},
filter: null,
renderItem: null,
limit: false,
async: false,
showAvatar: false
}
export default TeacherSearchSelect;
.teacher-search-select {
.common-select-menu{
.teacher-name, .subject-name {
width:150px;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis
}
}
}
\ No newline at end of file
import React from 'react';
import LiveCourseFilter from './components/LiveCourseFilter';
import LiveCourseOpt from './components/LiveCourseOpt';
// import LiveCourseList from './components/LiveCourseList';
class LiveCoursePage extends React.Component {
constructor(props) {
super(props);
// const { instId, teacherId } = window.currentUserInstInfo;
this.state = {
courseList: [], // 直播课列表
query: {
current: 1,
size: 10,
instId:0,
teacherId:0
},
total: 0,
loading: true,
}
}
// componentWillMount() {
// this.handleFetchLiveList();
// }
// // 获取直播课列表
// handleFetchLiveList = (_query) => {
// const { query } = this.state;
// const { teacherId } = window.currentUserInstInfo;
// const params = {
// teacherId: teacherId ? teacherId : null,
// ...query,
// ..._query,
// };
// this.setState({ query: params });
// window.axios
// .Apollo("public/businessLive/getLargeClassLiveList", params)
// .then((res) => {
// const { result: { records = [], total } } = res;
// this.setState({
// total,
// courseList: records
// });
// }) .finally(() => {
// this.setState({ loading: false });
// });
// }
render() {
const { query, total, courseList } = this.state;
return (
<div className="page big-live-page">
<div className="content-header">大班直播</div>
<div className="box">
<LiveCourseFilter
onChange={this.handleFetchLiveList}
/>
<LiveCourseOpt />
{/* <LiveCourseList
query={query}
total={total}
courseList={courseList}
onChange={this.handleFetchLiveList}
/> */}
</div>
</div>
)
}
}
export default LiveCoursePage;
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-15 17:29:12
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-15 18:16:05
* @Description: 新建/编辑直播课-基本信息
*/
import React from 'react';
import { Input, Button, message } from 'antd';
import UploadOss from "@/core/upload";
import { ImgCutModalNew } from '@/components';
import './AddLiveBasic.less';
const defaultCover = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class AddLiveBasic extends React.Component {
constructor(props) {
super(props);
this.state = {
xhr: null,
imageFile: null,
showCutModal: false
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 使用默认封面图
handleResetCoverUrl = () => {
const { data: { coverUrl } } = this.props;
const isDefaultCover = coverUrl === defaultCover;
// 如果已经是默认图的话,不做任何任何处理
if (isDefaultCover) return;
message.success('已替换为默认图');
this.props.onChange('coverId', null, defaultCover);
}
componentWillUnmount() {
const { xhr } = this.state;
xhr && xhr.abort();
}
render() {
const { showCutModal, imageFile } = this.state;
const { data, liveScene } = this.props;
const { courseName, coverUrl } = data;
const fileName = '';
// 是否是互动班课,互动班课不显示封面图
const isInteractive = liveScene === 'interactive';
// 当前是否使用的是默认图片
const isDefaultCover = coverUrl === defaultCover;
return (
<div className="add-live__basic-info">
<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.props.onChange('courseName', e.target.value)}}
/>
</div>
{
!isInteractive &&
<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">
<input
type="file"
value={fileName} // 避免选择同一文件 value不改变 不触发onChange事件
accept="image/png, image/jpeg, image/bmp, image/jpg"
ref="stagePicInputFile"
style={{display: 'none'}}
onChange={(event) => { this.handleShowImgCutModal(event) }}
/>
<Button onClick={() => {
this.setState({
currentInputFile: this.refs.stagePicInputFile
});
this.refs.stagePicInputFile.click();
}}>上传图片</Button>
<span
className={`default-btn ${isDefaultCover ? 'disabled' : ''}`}
onClick={this.handleResetCoverUrl}
>使用默认图</span>
<div className="tips">建议尺寸690*398像素,图片支持jpg、jpeg、png格式。</div>
</div>
</div>
</div>
}
<ImgCutModalNew
title="裁剪"
width={550}
cutWidth={500}
cutHeight={282}
cutContentWidth={500}
cutContentHeight={300}
visible={showCutModal}
imageFile={imageFile}
bizCode='LIVE_COURSE_MEDIA'
onOk={(urlStr, resourceId) => {
this.setState({ showCutModal: false });
this.props.onChange('coverId', resourceId, urlStr);
this.state.currentInputFile.value = '';
}}
onClose={() => this.setState({ showCutModal: false })}
reUpload={() => { this.state.currentInputFile.click() }}
/>
</div>
)
}
}
export default AddLiveBasic;
\ No newline at end of file
.add-live__basic-info {
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.course-cover {
margin-left: 14px;
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: #FF7519;
cursor: pointer;
&.disabled {
color: #CCC;
cursor: not-allowed;
}
}
.ant-upload-list {
display: none;
}
}
.tips {
margin-top: 8px;
color: #999;
}
}
}
#imgCutModalNew {
width: 500px;
height: 300px;
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-15 17:44:24
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-23 22:06:33
* @Description: 添加直播课-上课信息
*/
import React from 'react';
import {
Radio, Slider, Row, Input, InputNumber,
DatePicker, TimePicker, Select, Button,
message, Spin, Tooltip
} from 'antd';
import TeacherSelectV5 from "@/modules/classManage_V5/classDetail/TeacherSelectV5";
import ChargeExplainModal from '../modal/ChargeExplainModal';
import SelectStudent from '../modal/select-student/index';
import MultipleDatePicker from '@/modules/class/MultipleDatePicker';
import moment from 'moment';
import './AddLiveClass.less';
const defaultQuery = {
instId,
size: 10,
current: 1,
adminName: null
}
const { instId } = window.currentUserInstInfo;
class AddLiveClass extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
hasNext: false,
isUltimateEdition: false, // 是否是旗舰版
query: defaultQuery,
assistantList: [], // 助教老师列表
_assistantList: [],
addLiveType: props.addLiveType
}
}
componentWillMount() {
const { liveScene } = this.props;
// 如果是互动班课,则请求用户是否是旗舰版
if (liveScene === 'interactive') {
this.handleFetchPermission();
}
this.getAssistantList();
}
componentWillReceiveProps(nextProps) {
if(nextProps.data.assistant.sort().toString() !== this.props.data.assistant.sort().toString()) {
// 获取助教老师列表
this.getAssistantList(1, nextProps.data.assistant);
}
}
// 获取用户是否是旗舰版的权限
handleFetchPermission = () => {
const { instId } = window.currentUserInstInfo;
this.setState({ loading: true });
axios.Business('public/inst/checkInstProduct', {
instId: instId || LS.get("instId"),
productCodeList: ['ULTIMATESELL', 'PIP_TO_ULTIMATE', 'HIGH_TO_ULTIMATE']
}).then((res) => {
const { result } = res;
this.setState({
loading: false,
isUltimateEdition: result,
// 旗舰版用户没有大班直播模式,所以默认是大班互动模式
addLiveType: result ? 'LARGE_CLASS_INTERACTION' : 'LARGE_CLASS_LIVE'
})
});
}
// 获取助教老师列表
getAssistantList = (current = 1, selectList) => {
const { query, assistantList } = this.state;
const { selectedAssistant } = this.props;
const { teacherId, assistant } = this.props.data;
const idList = selectList ? selectList : assistant;
const _query = {
...query,
current,
idList,
size: idList.length <= 10 ? 10 : idList.length + 10
};
this.setState({ query: _query });
axios.Apollo("public/businessLive/queryAdminByName", _query).then((res) => {
const { result = {} } = res;
const { records = [], total = 0, hasNext } = result;
const list = current > 1 ? assistantList.concat(records) : records;
const _assistantList = _.uniq(
selectedAssistant.concat(
_.reject(list, (item) => item.id === teacherId)
),
false,
(item) => item.id
);
this.setState({
hasNext,
assistantList: list,
_assistantList: _assistantList
})
});
}
// 修改直播模式
handleChangeLiveType = (e) => {
this.setState({
addLiveType: e.target.value,
});
this.props.onChange('liveType', e.target.value);
};
//查看计费说明
handleLookExplain = () => {
const explainModal = (
<ChargeExplainModal
close={() => {
this.setState({
explainModal: null,
});
}}
/>
);
this.setState({ explainModal });
};
// 滑动加载更多
handleScroll = (e) => {
console.log('srcoll')
const { hasNext } = this.state;
const container = e.target;
const scrollToBottom = container && container.scrollHeight <= container.clientHeight + container.scrollTop;
if (scrollToBottom && hasNext) {
const { query } = this.state;
this.getAssistantList(query.current + 1);
}
}
// 选择学员数量
handleShowSelectStuModal = (studentType) => {
const {
after,
liveScene,
data: {
id,
studentList,
consumeStudentList,
excludeStudentIds,
excludeConsumeStudentIds,
}
} = this.props;
const { savedSelectedRows } = this.state;
const studentModal = (
<SelectStudent
savedSelectedRows={savedSelectedRows}
studentType={studentType}
liveScene={liveScene}
after={after} //表明是不是上课后的状态
excludeStudentIds={excludeStudentIds}
excludeConsumeStudentIds={excludeConsumeStudentIds}
studentList={studentList}
consumeStudentList={consumeStudentList}
close={() => {
this.setState({
studentModal: null,
});
}}
onSelect={(studentIds, selectedStudents, savedSelectedRows) => {
this.setState({ savedSelectedRows })
this.handleSelectStudent(studentIds, selectedStudents, studentType)
}}
/>
)
this.setState({ studentModal });
}
handleSelectStudent = (studentIds, selectedStudents = [], studentType) => {
let studentList = [];
if (studentType === 'DEDUCTION') {
studentList = selectedStudents
} else {
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
}
const {
liveScene,
liveType,
endTime,
startTime,
id,
podium,
excludeStudentIds,
excludeConsumeStudentIds
} = this.props.data;
const { addLiveType } = this.state;
// 当前选择的学员
const currentSelectStuIds = studentType === 'DEDUCTION' ? _.pluck(selectedStudents, 'studentId') : studentIds;
// 如果当前选择的是扣课时学员,那么总的已选学员人数 = 扣课时 + 之前选择不扣课时的
// 如果当前选择的是不扣课时学员,那么总的已选学员人数 = 不扣课时 + 之前选择的扣课时的
const prevSelectStutIds = studentType === 'DEDUCTION' ? excludeStudentIds : excludeConsumeStudentIds;
const studentLen = [...currentSelectStuIds, ...prevSelectStutIds].length;
// 如果是互动班课,模式为大班直播,学员人数超过1000人,提示最多选择1000人
// 如果是大班直播(大班直播没有模式,只要考虑学员人数是否超过了1000个人即可)
if ((liveScene === 'interactive' &&
addLiveType !== "SMALL_CLASS_INTERACTION" &&
studentLen > 1000) || (studentLen > 1000)
) {
message.info(`最多选择1000人`);
return;
// 如果是互动班课,模式为大班互动,学员人数超过课上台人数,提示最多选择上台人数
} else if (liveScene === 'interactive' &&
addLiveType === "SMALL_CLASS_INTERACTION" &&
studentLen > podium
) {
message.info(`最多选择${podium}人`);
return;
}
this.setState({ studentModal: null });
if (studentType === 'DEDUCTION') {
this.props.onChange('consumeStudentList', studentList);
} else {
this.props.onChange('studentList', studentList);
}
}
disabledDate = (current) => {
return current.valueOf() < moment().subtract(1, "days")
};
selectMultiDate = (calendarTime) => {
this.setState({
calendarTime
})
}
render() {
const {
loading,
assistantList,
addLiveType,
isUltimateEdition,
query,
_assistantList
} = this.state;
// pageType: 页面类型:add->新建、edit->编辑
// liveScene: 直播场景: interactive -> 互动班课 large -> 大班直播
// data:表单数据
// selectedAssistant: 已经选择的助教
const { pageType, liveScene, data, selectedAssistant, isXiaomai, isEdit, after } = this.props;
const {
podium,
endTime,
liveType,
liveDate,
assistant,
startTime,
teacherId,
studentList,
teacherName,
timeHorizonEnd,
consumeHourNum,
consumeClassTime,
timeHorizonStart,
consumeStudentList,
applyMode,
calendarTime
} = data;
// 已选择的上课学员数量(不扣课时)
const hasSelectedStu = studentList.length;
// 已选择的上课学员数量(扣课时)
const hasSelectedDeductionStu = consumeStudentList.length;
return (
<Spin spinning={loading}>
<div className="add-live__class-info">
{
liveScene === 'interactive' &&
<div className="live-mode">
<span className="label">直播模式:</span>
{
pageType === 'add' ?
<Radio.Group
className="uncommon-wrapper"
onChange={this.handleChangeLiveType}
value={addLiveType}
>
{
// 旗舰版不显示大班互动
!isUltimateEdition &&
<Radio
value={"LARGE_CLASS_LIVE"}
>
<p className="title">
{ window.ENUM.liveType["LARGE_CLASS_LIVE"].name }
</p>
<p className="info">
{ window.ENUM.liveType["LARGE_CLASS_LIVE"].info }
</p>
</Radio>
}
<Radio
value={"LARGE_CLASS_INTERACTION"}
>
<p className="title">
{ window.ENUM.liveType["LARGE_CLASS_INTERACTION"].name }
</p>
<p className="info">
{ window.ENUM.liveType["LARGE_CLASS_INTERACTION"].info }
</p>
</Radio>
<Radio
value={"SMALL_CLASS_INTERACTION"}
>
<p className="title">
{ window.ENUM.liveType["SMALL_CLASS_INTERACTION"].name }
</p>
<p className="info">
{ window.ENUM.liveType["SMALL_CLASS_INTERACTION"].info }
</p>
</Radio>
</Radio.Group> :
<div className="edit-info">
<p className="name">
{window.ENUM.liveType[liveType].name}
</p>
<p className="info">
{window.ENUM.liveType[liveType].info}
</p>
</div>
}
</div>
}
{
liveScene === 'interactive' &&
addLiveType !== "LARGE_CLASS_LIVE" &&
pageType === 'add' &&
<div className="stage-num">
<div className="content">
<span className="label">上台人数:</span>
<Row type="flex">
<Slider
min={1}
max={12}
style={{ width: 160 }}
className="add-live-slider"
onChange={(value) => {
this.props.onChange('podium', Number(value));
}}
value={typeof podium === "number" ? podium : 1}
/>
<Input
style={{ margin: "0 16px", width: 90 }}
value={podium}
onChange={(e) => {
const { value } = e.target;
this.props.onChange('podium', parseInt(value));
}}
onBlur={(e) => {
let value = e.target.value;
if (value < 1) {
value = 1;
} else if (value > 12) {
value = 12;
}
value = value ? parseInt(value) : value;
this.props.onChange('podium', value);
}}
/>
</Row>
</div>
<div className="tips">
请按需选择同时上台的最多人数,人数越多价格越贵,设置后不可修改。
<span
className="look-explain"
onClick={this.handleLookExplain}
>
查看计费规则
</span>
</div>
</div>
}
{
liveScene === 'interactive' &&
addLiveType !== "LARGE_CLASS_LIVE" &&
pageType === "edit" &&
<div className="podium-max-num">
<span className="label">上台人数:</span>
<span>最多可同时上台{podium}</span>
</div>
}
{
window.NewVersion && pageType === 'add' &&
<div className="course">
<div className="day">
<span className="label">
<span className="require">*</span>
上课日期
<Tooltip
overlayStyle={{maxWidth: 300, zIndex: '9999'}}
title={<div style={{width: '266px'}}>支持按上课日期批量创建直播课,创建后按“课程名称_日期”命名,例如:<br/>张三的语文课_9月18日<br/>张三的语文课_9月19日......</div>}>
<span className="iconfont">&#xe6f2;</span>
</Tooltip>
</span>
<div>
<div className='select-day'>
已选
<span class="mark-day">
{isLongArr(calendarTime)
? calendarTime.length : 0
}
</span>
</div>
<MultipleDatePicker
selectDateList={calendarTime}
onSelect={this.selectMultiDate}
canSelectTodayBefore={false}
/>
</div>
</div>
<div className="hour" id="hour">
<span className="label"><span className="require">*</span>上课时间:</span>
<TimePicker
format="HH:mm"
value={startTime ? moment(startTime) : null}
placeholder="开始时间"
style={{ width: 100, minWidth: 100}}
onChange={(time) => {
this.props.onChange('startTime', time);
}}
/>&nbsp;&nbsp;~&nbsp;&nbsp;
<TimePicker
format="HH:mm"
value={endTime ? moment(endTime) : null}
placeholder="结束时间"
style={{ width: 100, minWidth: 100 }}
onChange={(time) => {
this.props.onChange('endTime', time)
}}
/>
</div>
</div>
}
{
(window.NewVersion && pageType !== 'add' || !window.NewVersion) &&
<div className="time" id="time">
<div className="content">
<span className="label"><span className="require">*</span>上课时间:</span>
<DatePicker
disabled={liveScene === 'large' && !isEdit}
format="YYYY-MM-DD"
value={liveDate ? moment(Number(liveDate)) : null}
style={{ width: 160, minWidth: 130, marginRight: 10 }}
placeholder="上课日期"
getCalendarContainer={() =>
document.getElementById("time")
}
disabledDate={this.disabledDate}
onChange={(date) => { this.props.onChange('liveDate', date) }}
/>
<TimePicker
disabled={liveScene === 'large' && !isEdit}
format="HH:mm"
value={timeHorizonStart ? moment(Number(timeHorizonStart)) : null}
defaultOpenValue={moment(new Date().setHours(0,0,0,0))}
placeholder="开始时间"
style={{ width: 100, minWidth: 100, marginRight: 10 }}
getPopupContainer={() =>
document.getElementById("time")
}
onChange={(time) => { this.props.onChange('timeHorizonStart', time) }}
/>
<TimePicker
disabled={liveScene === 'large' && !isEdit}
format="HH:mm"
value={timeHorizonEnd ? moment(Number(timeHorizonEnd)) : null}
defaultOpenValue={moment(new Date().setHours(0,0,0,0))}
placeholder="结束时间"
style={{ width: 100, minWidth: 100 }}
getPopupContainer={() =>
document.getElementById("time")
}
onChange={(time) => { this.props.onChange('timeHorizonEnd', time) }}
/>
</div>
</div>
}
{
liveScene === 'interactive' &&
<div className="tips">
上课老师可以在 直播时 主动结束直播
</div>
}
<div className="teacher">
<span className="label"><span className="require">* </span>上课老师:</span>
<TeacherSelectV5
disabled={liveScene === 'large' && !isEdit}
ref="TeacherSelect"
showSearch={true}
allowClear={true}
onSelect={(teacherId, dataSet) => { this.props.onChange('teacherId', teacherId, dataSet) }}
placeholder="请选择上课老师"
defaultValue={teacherId}
style={{ width: 240, marginTop: 6 }}
/>
</div>
{
isXiaomai &&
<div className="assistant-teacher">
<span className="label">助教老师:</span>
<Select
id="assistant"
disabled={liveScene === 'large' && !isEdit}
mode="multiple"
value={assistant}
placeholder="请选择助教老师"
style={{ width: 240, marginTop: 6 }}
filterOption={(input, option) => option}
onPopupScroll={this.handleScroll}
onChange={(value) => {
this.props.onChange('assistant', value)
}}
onSearch={(value) => {
query.adminName = value
this.setState({
query
}, () => {
this.getAssistantList()
})
}}
onBlur={() => {
query.adminName = ''
this.setState({
query
}, () => {
this.getAssistantList()
})
}}
>
{_.map(_assistantList, (item, index) => {
return (
<Select.Option value={item.id} key={item.id}>{item.adminName}</Select.Option>
);
})}
</Select>
</div>
}
{
liveScene === 'large' &&
<div className="watch-setting">
<span className="label"><span className="require">* </span>分享设置:</span>
<div className="content">
<Radio.Group
onChange={(e) => this.props.onChange('applyMode', e.target.value)}
value={applyMode}
>
<Radio value={'ANYONE'}>
<div className="radio-item">
<div className="text mr16">允许任何学员通过直播课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端、“每课学堂”app、直播课网页端、直播课链接观看,非在读学员仅可通过直播课链接观看</div>
</div>
</Radio>
<Radio value={'ONLY_READ'}>
<div className="radio-item">
<div className="text mr16">仅在读学员可通过直播课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端、“每课学堂”app、直播课网页端、直播课链接观看</div>
</div>
</Radio>
<Radio value={'ASSIGN'}>
<div className="radio-item mb0">
<div className="text mr16">仅指定学员可通过直播课链接加入学习</div>
<div className="sub-text">加入后,在读学员可通过家长端、“每课学堂”app、直播课网页端、直播课链接观看</div>
</div>
</Radio>
</Radio.Group>
</div>
</div>
}
{
window.NewVersion && isXiaomai ?
<div className="class-student">
<span className="label">上课学员:</span>
<div className="class-student-content">
<div className="deduction-student">
<span className="label">扣课时学员:</span>
<div className="content">
<div className="has-selected">
已选择 <span style={{ color: "#FF8534" }}>{hasSelectedDeductionStu}</span>名学员
<Button onClick={() => this.handleShowSelectStuModal('DEDUCTION')}>选择学员</Button>
</div>
</div>
</div>
<div className="deduction-student-tips">
学员“到课”后,系统自动扣
<InputNumber
disabled={liveScene === 'large' && !isEdit}
min={0.1}
max={10}
precision={1}
value={consumeHourNum}
onChange={(value) => {
this.props.onChange('consumeHourNum', value)
}}
onBlur={(e) => {
this.props.onChange('consumeHourNum', e.target.value || 1)
}}
/>课时
</div>
<div className="no-deduction-student">
<span className="label">不扣课时学员:</span>
<div className="content">
<div className="has-selected">
已选择 <span style={{ color: "#FF8534" }}>{hasSelectedStu}</span>名学员
</div>
<Button onClick={this.handleShowSelectStuModal}>选择学员</Button>
</div>
</div>
</div>
</div> :
<div className="student">
<span className="label">上课学员:</span>
<div className="content">
<div className="has-selected">
已选择 <span style={{ color: "#FF8534" }}>{hasSelectedStu}</span>名学员
</div>
<Button onClick={this.handleShowSelectStuModal}>选择学员</Button>
</div>
</div>
}
{
isXiaomai &&
<div className="arrive-rule">
<span className="label"><span className="require">* </span>到课规则:</span>
学员累计在线时长达到
<InputNumber
className="arrive-rule-input"
disabled={liveScene === 'large' && !isEdit}
min={0}
max={999}
precision={0}
value={consumeClassTime}
onChange={(value) => {
this.props.onChange('consumeClassTime', value)
}}
onBlur={(e) => {
this.props.onChange('consumeClassTime', e.target.value || 30)
}}
/>
分钟,则视为学员”到课“
</div>
}
{ this.state.studentModal }
{ this.state.explainModal }
</div>
</Spin>
)
}
}
export default AddLiveClass;
\ No newline at end of file
.add-live__class-info {
margin-top: 12px;
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.class-student {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.class-student-content {
padding: 20px 24px 20px 16px;
border-radius: 2px;
border: 1px solid #eee;
.deduction-student-tips {
margin: 8px 0 16px 100px;
width: fit-content;
height: 52px;
line-height: 52px;
padding: 0 10px;
background-color: #F0F2F5;
.ant-input-number {
margin: 0 4px;
}
}
}
}
.live-mode,
.podium-max-num,
.teacher,
.assistant-teacher{
margin: 12px 0;
}
.watch-setting {
display: flex;
margin-bottom: 16px;
.ant-radio-group {
display: flex;
flex-direction: column;
.radio-item {
margin-bottom: 12px;
.text {
color: #333;
}
.sub-text {
color: #999;
}
}
.ant-radio {
vertical-align: top;
padding-top: 2px;
}
}
}
.label {
width: 100px;
text-align: right;
.require {
color: #EC4B35;
}
}
.tips {
margin-left: 100px;
margin-top: 8px;
color: #666;
}
.uncommon-wrapper .ant-radio-wrapper {
width: 290px;
height: 96px;
padding: 6px 12px;
margin-bottom: 10px;
background-color: #fafafa;
vertical-align: top;
box-sizing: border-box;
margin-right: 16px;
.ant-radio {
vertical-align: -2px;
}
span:last-child {
vertical-align: top;
}
.title {
font-size: 14px;
color: #333;
line-height: 20px;
.tag {
display: inline-block;
padding: 0 6px;
margin-left: 4px;
font-size: 11px;
color: #fff;
line-height: 20px;
background-color: #ccc;
border-radius: 2px;
}
}
.info {
width: 100%;
padding-top: 4px;
margin-left: -22px;
white-space: normal;
font-size: 14px;
color: #666;
line-height: 20px;
}
}
.live-mode {
display: flex;
.name {
color: #333;
}
.info {
color: #666;
}
}
.stage-num {
margin-bottom: 10px;
.content {
display: flex;
align-items: center;
}
.add-live-slider {
.ant-slider-track {
background-color: #FC9C6B;
}
.ant-slider-handle{
background:linear-gradient(180deg,rgba(255,180,103,1) 0%,rgba(255,145,67,1) 100%);
border:none;
width: 10px;
height: 10px;
margin: -3px;
box-shadow: none;
}
}
}
.teacher,
.student,
.deduction-student,
.no-deduction-student {
display: flex;
align-items: center;
.content {
display: flex;
align-items: center;
.ant-btn {
margin-left: 8px;
}
}
}
textarea.ant-input {
min-height: 80px;
}
.look-explain {
color: #FC9C6B;
cursor: pointer;
}
.course {
.day {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.select-day {
margin-bottom: 4px;
.mark-day {
color: #FC9C6B;
}
}
}
}
.iconfont {
color: #bfbfbf;
cursor: pointer;
}
.arrive-rule-input {
margin: 0 4px;
}
.multiple-calendar {
line-height: 40px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-16 11:05:17
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-24 14:29:52
* @Description: 添加直播-简介
*/
import React from 'react';
import { Input, message, Upload, Radio, Row, Col, Button, Popover, Switch } from 'antd';
import UploadOss from "@/core/upload";
import EditorBox from '../components/EditorBox';
import './AddLiveIntro.less';
import SelectPrepareFileModal from '../prepare-lesson/modal/SelectPrepareFileModal';
import { DISK_MAP } from '@/common/constants/academic/lessonEnum';
import { ImgCutModalNew } from '@/components';
const { TextArea } = Input;
const defaultCover = 'https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1599635741526.png';
class AddLiveIntro extends React.Component {
constructor(props) {
super(props);
this.state = {
warmUrl: defaultCover,
showSelectFileModal: false,
diskList: [],
}
}
// 上传封面图
handleShowImgCutModal = (event) => {
const imageFile = event.target.files[0];
if (!imageFile) return;
this.setState({
imageFile,
showCutModal: true,
});
}
// 选择暖场资源
handleSelectVideo = (file) => {
this.setState({
showSelectFileModal: false
})
const { ossUrl, resourceId, folderName, folderFormat, folderSize } = file;
const liveCourseWarmMedia = {
contentType: 'WARMUP',
mediaType: folderFormat === 'MP4' ? 'VIDEO' : 'PICTURE',
mediaContent: resourceId,
mediaUrl: ossUrl,
mediaName: folderName,
size: folderSize
}
this.props.onChange('liveCourseWarmMedia', liveCourseWarmMedia);
}
// 获取机构可见的磁盘
handleFetchDiskList = () => {
axios.Apollo('public/apollo/getUserDisk', {}).then((res) => {
const { result = [] } = res;
const diskList = result.map((item) => {
return {
...item,
folderName: DISK_MAP[item.disk]
}
});
this.setState({ diskList });
});
}
// 删除简介
handleDeleteIntro = (index) => {
const { liveCourseMediaRequests } = this.props.data;
liveCourseMediaRequests.splice(index, 1);
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
// 上移简介
handleMoveUpIntro = (index) => {
const { liveCourseMediaRequests } = this.props.data;
const prevItem = liveCourseMediaRequests[index];
const nextItem = liveCourseMediaRequests[index + 1];
liveCourseMediaRequests.splice(index, 2, nextItem, prevItem);
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
// 下移简介
handleMoveDownIntro = (index) => {
const { liveCourseMediaRequests } = this.props.data;
const prevItem = liveCourseMediaRequests[index - 1];
const nextItem = liveCourseMediaRequests[index];
liveCourseMediaRequests.splice(index - 1, 2, nextItem, prevItem);
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
renderLittleIcon = (index) => {
const { liveCourseMediaRequests } = this.props.data;
return (
<div className="little-icon">
<span
className="icon iconfont close"
onClick={() => { this.handleDeleteIntro(index); }}
></span>
{
index > 0 &&
<span
className="icon iconfont"
onClick={() => { this.handleMoveDownIntro(index); }}
>&#xe6d1;</span>
}
{
index !== liveCourseMediaRequests.length - 1 &&
<span
className="icon iconfont"
onClick={() => { this.handleMoveUpIntro(index); }}
>&#xe6cf;</span>
}
</div>
)
}
handleChangeIntro = (index, value, length) => {
const { liveCourseMediaRequests } = this.props.data;
liveCourseMediaRequests[index].mediaContent = value;
liveCourseMediaRequests[index].mediaContentLength = length
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
handleAddIntroText = () => {
const { liveCourseMediaRequests } = this.props.data;
liveCourseMediaRequests.push({
mediaType: 'TEXT',
mediaContent: '',
key: window.random_string(16)
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
}
handleUpload = (Blob) => {
// 最多添加九图片
const { liveCourseMediaRequests } = this.props.data;
const list = _.filter(liveCourseMediaRequests, (item) => {
return item.mediaType == "PICTURE";
});
if (list.length > 8) {
message.warning("最多添加9张图片");
return;
}
const { instId } = window.currentUserInstInfo;
const { name, size } = Blob;
const resourceName = window.random_string(16) + name.slice(name.lastIndexOf('.'));
const params = {
resourceName,
accessTypeEnum: 'PUBLIC',
bizCode: 'LIVE_COURSE_MEDIA',
instId: instId || LS.get('instId'),
}
window.axios.Apollo("public/apollo/commonOssAuthority", params).then((res) => {
const { resourceId } = res.result;
const signInfo = res.result;
// 构建上传的表单
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("OSSAccessKeyId", signInfo.accessId);
formData.append("policy", signInfo.policy);
formData.append("callback", signInfo.callback);
formData.append("Signature", signInfo.signature);
formData.append("key", signInfo.key);
formData.append("file", Blob);
formData.append("success_action_status", 200);
xhr.open("POST", signInfo.host);
xhr.onload = () => {
liveCourseMediaRequests.push({
size,
mediaName: name,
mediaContent: resourceId,
mediaType: 'PICTURE',
mediaUrl: window.URL.createObjectURL(Blob),
});
this.props.onChange('liveCourseMediaRequests', liveCourseMediaRequests);
};
xhr.onerror = () => {
xhr.abort();
};
xhr.send(formData);
this.setState({ xhr })
});
}
componentWillMount() {
this.handleFetchDiskList();
}
componentWillUnmount() {
const { xhr } = this.state;
xhr && xhr.abort();
}
render() {
const { liveScene, liveType, isXiaomai, isEdit, data: { introduction, needRecord, whetherRecord, liveCourseMediaRequests = [], liveCourseWarmMedia = {}, isAutoSendReport } } = this.props;
const { showCutModal, warmUrl, showSelectFileModal, diskList, imageFile } = this.state
// 是否是互动班课
const isInteractive = liveScene === 'interactive';
return (
<div className="add-live__intro-info">
{(liveScene === 'large' || (liveScene === 'interactive' && liveType === 'LARGE_CLASS_LIVE')) && isXiaomai &&
<div className="playback">
<span className="label">直播回放:</span>
<div className="content">
<Radio.Group disabled={liveScene === 'large' && !isEdit} value={needRecord} onChange={(e) => { this.props.onChange('needRecord', e.target.value) }}>
<Row style={{ marginBottom: '5px' }}>
<Col span={8}>
<Radio value="YES">
自动录制
</Radio>
</Col>
<Col span={16}>
<span className="playback__text">系统自助进行全程直播录制</span>
</Col>
</Row>
<Row>
<Col span={8}>
<Radio value="NO">
手动录制
</Radio>
</Col>
<Col span={16}>
<span className="playback__text">老师手动选择何时开始录制</span>
</Col>
</Row>
</Radio.Group>
</div>
</div>
}
{
(liveScene === 'interactive' && liveType !== 'LARGE_CLASS_LIVE' && isXiaomai) &&
<div className="interactive-playback">
<span className="label">直播回放:</span>
<div className="content">
<Radio.Group disabled={liveScene === 'large' && !isEdit} value={whetherRecord} onChange={(e) => { this.props.onChange('whetherRecord', e.target.value) }}>
<Radio value="YES">
自动录制
</Radio>
<Radio value="NO">
不录制
</Radio>
</Radio.Group>
<Popover content={
<div className="record-rule-wrap">
<p>录制费 = 录课单价 x 回放视频时长</p>
<ul>
<li>录课单价:2元/小时</li>
<li>回放视频时长:0.5小时起收,不足0.5小时的按0.5小时结算</li>
</ul>
<p className="text">示例:生成了49分26秒的回放视频,不足1小时按1小时计算,录制费就是2元</p>
</div>
}>
<span className="check-record-rule">查看录制费规则</span>
</Popover>
</div>
</div>
}
{
((liveScene === 'large' || liveScene === 'interactive') && isXiaomai && window.currentUserInstInfo.saaSVersionEnum === 'V_50') &&
<div className="auto-send-class-report">
<span className="label">自动发送报告:</span>
<Switch
checked={isAutoSendReport}
onChange={(checked) => this.props.onChange("isAutoSendReport",checked)}></Switch>
<div className="open-text">开启:课程结束后,公众号将自动发送课堂报告给上课学员(仅已绑定微信号的学员)</div>
<div className="close-text">关闭:不自动发送,但学员仍可通过课次详情页查看课堂报告</div>
</div>
}
{ ((liveScene === 'large' || liveScene === 'interactive') && isXiaomai) &&
<div className="warmup">
<span className="label">直播暖场图:</span>
<div className="course-cover__wrap">
<div className="img-content" style={ liveCourseWarmMedia.mediaUrl ? {background: '#000'} : {} }>
<img src={liveCourseWarmMedia.mediaType === 'VIDEO' ? `${liveCourseWarmMedia.mediaUrl}?x-oss-process=video/snapshot,t_0,m_fast` : (liveCourseWarmMedia.mediaUrl ? liveCourseWarmMedia.mediaUrl : defaultCover )} />
{
liveCourseWarmMedia.mediaUrl &&
<div className="img-delete-wrap">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1600073872956.png" onClick={() => {
this.props.onChange('liveCourseWarmMedia', {});
}}/>
</div>
}
</div>
<div className="opt-btns">
<Button
disabled={liveScene === 'large' && !isEdit}
onClick={() => {
this.setState({
showSelectFileModal: true
})
}}>上传图片/视频</Button>
<div className="tips">建议尺寸1280*720px或16:9。图片最大5M,支持jpg、jpeg和png;视频最大500M,支持mp4。</div>
<Popover content={
<div className="example-wrap">
<p className="title">直播间暖场图示例</p>
<p className="text">直播开始前,展示在直播间视频区域</p>
<img src='https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1599644482652.png'></img>
</div>
}>
<div className="checkExample">查看示例</div>
</Popover>
</div>
</div>
</div>
}
<div className="introduce">
<span className="label">直播简介:</span>
<div className="content">
{
isInteractive &&
<TextArea
value={introduction}
placeholder="简单介绍下这次课吧"
maxLength={200}
style={{ width: 480 }}
onChange={(e) => { this.props.onChange('introduction', e.target.value) }}
/>
}
{
!isInteractive &&
[
<div className="intro-list">
{
liveCourseMediaRequests.map((item, index) => {
if (item.mediaType === 'TEXT') {
return (
<div className="intro-list__item" key={item.key}>
<EditorBox
detail={{
content: item.mediaContent
}}
onChange={(val, length) => { this.handleChangeIntro(index, val, length) }}
/>
{this.renderLittleIcon(index)}
</div>
)
}
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-list__item picture" key={index}>
<div className="img__wrap">
<img src={item.mediaUrl} />
</div>
{this.renderLittleIcon(index)}
</div>
)
}
})
}
</div>,
<div className="operate">
<div className="operate__item" onClick={this.handleAddIntroText}>
<span className="icon iconfont">&#xe760;</span>
<span className="text">文字</span>
</div>
<Upload
fileList={[]}
accept="image/jpeg, image/png, image/jpg, image/gif"
beforeUpload={(Blob) => {
this.handleUpload(Blob);
return false;
}}
>
<div className="operate__item">
<span className="icon iconfont">&#xe74a;</span>
<span className="text">图片</span>
</div>
</Upload>
</div>,
<div className="tips">
• 图片支持jpeg、jpg、png、gif格式
</div>
]
}
</div>
</div>
{/* 选择暖场图文件弹窗 */}
<SelectPrepareFileModal
operateType="select"
accept="video/mp4,image/jpeg,image/png,image/jpg"
selectTypeList={['MP4', 'JPG', 'JPEG', 'PNG']}
tooltip='支持文件类型:jpg、jpeg、png、mp4'
isOpen={showSelectFileModal}
diskList={diskList}
onClose={() => {
this.setState({ showSelectFileModal: false })
}}
onSelect={this.handleSelectVideo}
/>
</div>
)
}
}
export default AddLiveIntro;
.add-live__intro-info {
.playback {
margin-bottom: 10px;
&__text {
color: #999;
}
}
.playback,
.introduce {
display: flex;
flex-direction: row;
}
.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 E from 'wangeditor';
import './EditorBox.less';
class EditorBox extends React.Component {
constructor(props) {
super(props)
this.state = {
editorId: window.random_string(16),
textLength: 0,
}
}
componentDidMount() {
this.renderEditor()
}
renderEditor() {
const { editorId } = this.state;
const { detail, onChange } = this.props;
const editorInt = new E(`#editor${editorId}`);
editorInt.customConfig.menus = [
// 'head', // 标题
'bold', // 粗体
// 'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'list', // 列表
'justify', // 对齐方式
'emoticon', // 表情
]
editorInt.customConfig.emotions = [
{
title: 'emoji',
type: 'emoji',
content: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '😊', '🙂', '🙃', '😉', '😓', '😅', '😪', '🤔', '😬', '🤐']
}
]
editorInt.customConfig.zIndex = 1;
editorInt.customConfig.pasteFilterStyle = false;
editorInt.customConfig.pasteIgnoreImg = true;
// 自定义处理粘贴的文本内容
editorInt.customConfig.pasteTextHandle = function (content) {
if (content == '' && !content) return ''
var str = content
str = str.replace(/<xml>[\s\S]*?<\/xml>/ig, '')
str = str.replace(/<style>[\s\S]*?<\/style>/ig, '')
str = str.replace(/[ | ]*\n/g, '\n')
str = str.replace(/\&nbsp\;/ig, ' ')
return str
}
editorInt.customConfig.onchange = (html) => {
const textLength = editorInt.txt.text().replace(/\&nbsp\;/ig, ' ').length;
this.setState({ textLength }, () => {
onChange(html, this.state.textLength);
})
}
editorInt.create();
editorInt.txt.html(detail.content);
editorInt.change && editorInt.change();
}
render() {
const { editorId, textLength } = this.state;
const { limitLength = 1000 } = this.props;
return <div className="wang-editor-container ">
<div className="editor-box" id={`editor${editorId}`}></div>
{textLength > limitLength && <div className="editor-tips">超了{textLength - limitLength}个字</div>}
</div>
}
}
export default EditorBox;
.wang-editor-container {
border: 1px solid #E8E8E8;
border-radius: 4px;
width: 552px;
position: relative;
.w-e-text p,
.w-e-text h1,
.w-e-text h2,
.w-e-text h3,
.w-e-text h4,
.w-e-text h5,
.w-e-text table,
.w-e-text pre {
margin: 0;
}
.w-e-toolbar {
background-color: #fff !important;
border: none !important;
border-bottom: 1px solid #E8E8E8 !important;
}
.w-e-text-container {
border: none !important;
height: 88px !important;
}
.editor-tips {
position: absolute;
top: 5px;
right: 8px;
color: #f5222d;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:41:30
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-23 13:45:16
* @Description: 大班直播、互动班课列表的筛选组件
*/
import React from 'react';
import { withRouter } from 'react-router-dom';
import { Row, Input, Select } from 'antd';
import Bus from '@/core/bus';
import TeacherSearchSelect from "@/modules/common/TeacherSearchSelect";
import RangePicker from "@/modules/common/DateRangePicker";
import './LiveCourseFilter.less';
const { Search } = Input;
const { Option } = Select;
const defaultQuery = {
courseName: null,
startTime: null,
teacherName: null,
courseState: undefined,
}
class LiveCourseFilter extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {...defaultQuery}
}
}
componentWillReceiveProps(nextProps) {
const { match: { path } } = nextProps;
const { match: { path: curPath } } = this.props;
if (path !== curPath) {
this.setState({
query: {...defaultQuery}
})
}
}
// 改变搜索条件
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 } = this.state;
if (_.isEmpty(dates)) {
delete query.startTime;
delete query.endTime;
} else {
query.startTime = dates[0].valueOf();
query.endTime = dates[1].valueOf();
}
this.setState({
query,
current: 1,
}, () => {
this.props.onChange(this.state.query);
})
}
// 选择老师
handleSelectTeacher = (teacher) => {
const { name: teacherName, teacherId } = teacher;
this.setState({
query: {
...this.state.query,
teacherId,
teacherName,
current: 1,
}
}, () => {
this.props.onChange(this.state.query);
})
}
// 清空搜索条件
handleReset = () => {
this.setState({
query: {
...this.state.query,
courseName: null,
startTime: null,
endTime: null,
teacherId: null,
teacherName: null,
courseState: undefined,
current: 1,
},
}, () => {
this.props.onChange(this.state.query);
})
}
render() {
const {
courseName, startTime, endTime,
courseState, teacherName, teacherId
} = this.state.query;
const { teacherId: _teahcerId } = {};
const isTeacher = !!_teahcerId; // 判断是否是老师身份
return (
<div className="live-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% - 70px)" }}
/>
</div>
<div className="search-condition__item">
<span className="search-date">上课日期:</span>
<RangePicker
id="course_date_picker"
allowClear={false}
value={ startTime ? [moment(startTime), moment(endTime)] : null }
format={"YYYY-MM-DD"}
onChange={(dates) => { this.handleChangeDates(dates) }}
style={{ width: "calc(100% - 70px)" }}
/>
</div>
{!isTeacher &&
<div className="search-condition__item">
<TeacherSearchSelect
id="teacher_select"
ref="TeacherSelect"
label="上课老师"
placeholder="请选择"
teacherName={teacherName}
onSelect={this.handleSelectTeacher}
defaultValue={teacherId}
/>
</div>
}
<div className="search-condition__item">
<span className="select-status">上课状态:</span>
<Select
style={{ width: "calc(100% - 70px)" }}
placeholder="请选择"
allowClear={true}
value={courseState}
onChange={(value) => { this.handleChangeQuery('courseState', value) }}
>
<Option value="UN_START">待上课</Option>
<Option value="STARTING">上课中</Option>
<Option value="FINISH">已完成</Option>
<Option value="EXPIRED">未成功开课</Option>
</Select>
</div>
</div>
<span
className="icon iconfont"
onClick={this.handleReset}
>
&#xe6a3;
</span>
</Row>
</div>
)
}
}
export default withRouter(LiveCourseFilter);
\ No newline at end of file
.live-course-filter {
position: relative;
.search-condition {
width: 100%;
display: flex;
align-items: center;
flex-wrap: wrap;
&__item {
width: 30%;
margin-right: 3%;
margin-bottom: 12px;
}
}
.iconfont {
position: absolute;
right: 12px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:43:00
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-23 20:23:12
* @Description: 大班直播、互动班课的直播课列表
*/
import React from 'react';
import { Table, Modal, Tooltip, Badge, message, Dropdown, Button } from 'antd';
import Bus from '@/core/bus';
import User from "@/core/user";
import User_t from "@/teacher/core/user";
import { PageControl } from "@/components";
// import { LIVE_SHARE_MAP } from '@/common/constants/academic/cloudClass';
import DownloadLiveModal from '@/components/DownloadLiveModal';
import LiveStudentListModal from '../modal/LiveStudentListModal';
import CheckBalanceModal from '../modal/CheckBalanceModal';
import StartLiveModal from '../modal/StartLiveModal';
import ClassRecordModal from '../modal/ClassRecordModal';
import PlayBackRecordModal from '../modal/PlayBackRecordModal';
import ManageCoursewareModal from '../modal/ManageCoursewareModal';
import ShareLiveModal from '../modal/ShareLiveModal';
import AccountChargeModal from '../modal/AccountChargeModal';
import SelectStudent from '../modal/select-student';
import './LiveCourseList.less';
const { teacherId, instId, adminId, adminName, name } = window.currentUserInstInfo;
const isTeacher = !!teacherId;
const courseStateShow = {
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",
},
};
const ENV = process.env.DEPLOY_ENV || 'dev';
class LiveCourseList extends React.Component {
constructor(props) {
super(props);
this.state = {
url: '',
editData: {},
columns: [],
isXiaoMai: false,
downloadUrl: null,
openCoursewareModal: false,
currentTeacherId: teacherId
}
}
componentWillMount() {
}
componentDidMount() {
}
// 获取直播间类型
handleCheckLiveVersion = () => {
}
// 获取当前登录帐号的teacherId
getTeacherId = () => {
}
getDownloadVersion() {
}
parseColumns = () => {
const menu = (item) => (
<div className="live-course-more-menu">
<div
onClick={() => {
this.handleShowClassModal(item);
}}
>
上课记录
</div>
{item.haveRecord === 'YES' &&
<div
onClick={() => {
this.handleShowRepeatModal(item);
}}
>
回放记录
</div>
}
</div>
);
const columns = [
{
title: "课程名称",
width: "20%",
key: "courseName",
dataIndex: "courseName",
render: (val, record) => {
const { coverUrl = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png' } = record;
return (
<div className="record__item">
<img className="course-cover" src={coverUrl} />
<span className="course-name">{val}</span>
</div>
)
}
},
{
title: "上课时间",
width: "10%",
key: "classTime",
dataIndex: "classTime",
render: (val, item) => {
return `${formatDate("YYYY-MM-DD H:i",parseInt(item.startTime))}~${formatDate("H:i", parseInt(item.endTime))}`
},
},
{
title: "上课老师",
width: "8%",
key: "nickname",
dataIndex: "nickname",
},
{
title: (window.NewVersion && isXiaoMai) ? '学员管理' : '学员人数',
width: "9%",
key: "quota",
dataIndex: "quota",
render: (val, item) => {
return (
<span
className="operate-text"
onClick={() => {
this.handleLinkToClassData(item)
}}
>
{`${val}人`}
</span>
);
},
},
{
title: "课件管理",
width: "7%",
dataIndex: "courseware",
render: (val, item, index) => {
return item.channel === "XIAOMAI" ? (
<span
className="operate-text"
onClick={() => {
this.setState({
editData: item,
openCoursewareModal: true,
});
}}
>
{item.courseDocumentCount || 0}
</span>
) : (
<span style={{ color: "#999" }}>暂不支持</span>
);
},
},
{
title: "上课状态",
width: "10%",
key: "courseState",
dataIndex: "courseState",
render: (val, item) => {
const { currentTeacherId } = this.state;
const teacherPermission = item.teacherId === currentTeacherId;
return (
<div className="course-status">
<Badge
className="badge"
color={courseStateShow[val].color}
text={courseStateShow[val].title}
status={val == 'STARTING' ? 'processing' : undefined}
/>
</div>
);
},
},
{
title: "操作",
width: "20%",
key: "operate",
dataIndex: "operate",
render: (val, item) => {
return (
<div className="operate">
<div
key="enter_live_room1"
className="operate__item"
onClick={() => { this.handleCheckPreEnterLiveRoom(item, teacherPermission ? 1 : 2) }}
>进入直播间
</div>
<span className="operate__item split" key="enter_live_room1_split"> | </span>
<div
key="view_play_back"
className="operate__item"
onClick={() => { this.handleStartPlayBack(item); }}
>查看回放</div>,
<span className="operate__item split" key="view_play_back_split"> | </span>
<div
key="share"
className="operate__item"
onClick={() => { this.handleShowShareModal(item); }}
>
分享
</div>,
<span key="split1" className="operate__item split"> | </span>
<div className="big-live">
<Dropdown overlay={this.renderMoreOperate(item, isXiaoMai, otherPermission, isInteractive)}>
<span className="more-operate">
<span className="operate-text">更多</span>
<span
className="iconfont icon"
style={{ color: "#FC9C6B" }}
>
&#xe824;
</span>
</span>
</Dropdown>
</div>
</div>
)
}
}
];
}
renderMoreOperate = (item, isXiaoMai, otherPermission, isInteractive) => {
return (
<div className="live-course-more-menu">
<div
className="operate__item"
onClick={() => this.handleRemindClass(item)}
>群发通知</div>
<div
className="operate__item"
onClick={() => this.handleEditLiveClass(item, isInteractive)}
>编辑</div>
<div
className="operate__item"
onClick={() => this.handleDeleteLiveClass(item.liveCourseId)}
>删除</div>
</div>
)
}
// 显示添加学员的更多操作
renderAddStuOverLay = (item) => {
return (
<div className="live-course-more-menu">
<div
className="operate__item"
onClick={() => this.handleShowSelectStuModal(item, 'DEDUCTION')}
>
添加扣课时的学员
</div>
<div
className="operate__item"
onClick={() => this.handleShowSelectStuModal(item)}
>
添加不扣课时的学员
</div>
</div>
)
}
render() {
const { total, query, courseList, loading, type } = this.props;
const { current, size } = query;
const {
openCoursewareModal, openDownloadModal, editData,
downloadUrl, url, columns,
} = this.state;
return (
<div className="live-course-list">
<Table
bordered
size="middle"
pagination={false}
columns={columns}
loading={loading}
dataSource={courseList}
rowKey={(row) => row.liveCourseId}
/>
<div className="box-footer">
<PageControl
current={current - 1}
pageSize={size}
total={parseInt(total)}
toPage={(page) => {
const _query = {...query, current: page + 1};
this.props.onChange(_query)
}}
/>
</div>
</div>
)
}
}
export default LiveCourseList;
\ No newline at end of file
.live-course-list {
margin-top: 16px;
.record__item {
display: flex;
align-items: center;
}
.operate {
display: flex;
align-items: center;
flex-wrap: wrap;
.operate__item {
color: #ff7519;
cursor: pointer;
&.split {
margin: 0 8px;
color: #BFBFBF;
}
}
}
.operate-text {
color: #ff7519;
cursor: pointer;
}
.course-cover {
min-width: 90px;
max-width: 90px;
height: 50px;
border-radius: 2px;
margin-right: 8px;
}
.course-status {
display: flex;
.badge {
transform: none !important;
display: flex;
align-items: center;
.ant-badge-status-dot {
top: 0;
}
.ant-badge-status-text {
white-space: nowrap;
}
}
}
.course-start-end {
margin-left: 16px;
width: 78px;
height: 20px;
border-radius: 2px;
border: 1px solid rgba(204, 204, 204, 1);
display: flex;
align-items: center;
cursor: pointer;
white-space: nowrap;
.start-icon {
color: #3296fa;
font-size: 12px;
transform: scale(0.8);
margin: 0 5px;
}
.end-icon {
color: #00d700;
font-size: 12px;
transform: scale(0.8);
margin: 0 5px;
}
.start-end-text {
font-size: 12px;
}
}
}
.live-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;
}
}
}
.tipTitle {
.type {
font-weight: 700;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-14 15:42:24
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-23 14:35:28
* @Description: 大班直播、互动班课的操作区
*/
import React from 'react';
import { Button, Modal, message } from 'antd';
import './liveCourseOpt.less';
class LiveCourseOpt extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="live-course-opt">
<div className="opt__left">
<Button type="primary" onClick={this.handleCreateLiveCouese}>新建直播课</Button>
<Button onClick={this.handleDownloadClient}>下载直播客户端</Button>
</div>
</div>
)
}
}
export default LiveCourseOpt;
\ No newline at end of file
import React, { useEffect, useState } from 'react';
import { Modal, Button } from 'antd';
import qrcode from "@/libs/qrcode/qrcode.js";
import './StudentClassReportModal.less';
const StudentClassReportModal = (props) => {
const url = window.CONFIG.parentHref[window.CONFIG.env];
const previewUrl = `${url}#/live/pc-cloud-class-report?courseId=${props.courseId}&studentId=${props.studentId}`
const mobileUrl = `${url}#/live/cloud-class-report?courseId=${props.courseId}&studentId=${props.studentId}`
useEffect(() => {
window.axios.Sales('public/businessShow/convertShortUrls', {
urls: [mobileUrl]
}).then((res) => {
const { result = [] } = res;
const qrcodeNode = new qrcode({
text: result[0].shortUrl,
size: 106
});
document.querySelector('#qrcode').appendChild(qrcodeNode)
})
}, [])
return (
<Modal
visible={true}
title={props.studentId? "TA的课堂报告":"老师的课堂报告"}
width={560}
onCancel={props.onCancel}
className="student-class-report-modal"
footer={ <div onMouseEnter={(e) => {
e.preventDefault();
}}
>
<Button
key="cancel"
onMouseEnter={(e) => {
e.preventDefault();
let qrcode = document.getElementsByClassName("qrcode")[0];
qrcode.style.display = "block";
}}
onMouseLeave={(e) => {
e.preventDefault();
let qrcode = document.getElementsByClassName("qrcode")[0];
qrcode.style.display = "none";
}}
>
分享TA的课堂报告
</Button>
</div>}
>
<div className="modal-content">
<iframe
src={previewUrl}
style={{ width: '100%', height: '70vh', border: 'none' }}
/>
</div>
<div className="qrcode">
<div className="qrcode-text">微信扫码查看/分享</div>
<div id="qrcode"></div>
<div className="triangle"></div>
</div>
</Modal>
)
}
export default StudentClassReportModal;
\ No newline at end of file
.student-class-report-modal {
.modal-content {
height: 70vh;
margin-left: -3px;
margin-right: -3px;
position: relative;
}
.share-entry {
width: 560px;
height: 48px;
background: #ffffff;
border-radius: 0px 0px 6px 6px;
position: fixed;
left: 0;
bottom: -48px;
.share-btn {
width: 148px;
height: 28px;
background: #ffffff;
border: 1px solid #e8e8e8;
position: absolute;
right: 23px;
top: 10px;
width: 116px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 28px;
}
}
.qrcode {
display: none;
position: absolute;
bottom:70px;
right:0;
width: 200px;
height: 191px;
background: #FFFFFF;
padding: 14px 40px 18px 40px;
text-align: center;
border-radius:8px;
box-shadow: 0 0 6px 0 rgba(0,0,0,.2);
.qrcode-text {
width: 126px;
height: 21px;
font-size: 15px;
color: #333333;
line-height: 21px;
margin-bottom: 22px;
}
#qrcode {
width: 106px;
height: 106px;
margin: 0 auto;
}
.triangle {
position: absolute;
width: 10px;
height: 10px;
transform: rotate(45deg);
bottom: -5px;
left: 95px;
background-color: #fff;
box-shadow: 0 0 6px 0 rgba(0,0,0,.2);
}
}
}
.class-report-modal {
.modal-content {
height: 70vh;
margin-left: -3px;
margin-right: -3px;
}
}
import React, { useEffect, useState } from 'react';
import { Modal } from 'antd';
import './TeacherClassReportModal.less';
import TeacherClassReportPage from './TeacherClassReportPage';
interface TeacherClassReportModal {
onCancel: Function,
courseId: string,
}
const TeacherClassReportModal = (props: TeacherClassReportModal) => {
return (
<Modal
visible={true}
// title={props.studentId? "TA的课堂报告":"老师的课堂报告"}
width={560}
footer={null}
// onCancel={props.onCancel}
className="class-report-modal"
>
<div className="modal-content">
<TeacherClassReportPage courseId={props.courseId}/>
</div>
</Modal>
)
}
export default TeacherClassReportModal;
\ No newline at end of file
/*
* @Author: wufan
* @Date: 2020-11-04 15:53:17
* @Last Modified by: mikey.zhaopeng
* @Last Modified time: 2020-11-16 16:32:13
*/
import React, { useEffect, useState } from 'react';
import { Tooltip } from 'antd';
import Lottie from "lottie-web";
import './TeacherClassReportPage.less';
const TeacherClassReportPage = (props) => {
const [liveType, setLiveType] = useState('');
const [teacherName, setTeacherName] = useState('');
const [courseName, setCourseName] = useState(''); // 课次名称
const [classTime, setClassTime] = useState(); // 实际上课时间
const [actualStartTime, setActualStartTime] = useState(); // 实际开始上课时间
const [actualEndTime, setActualEndTime] = useState(); // 实际开始上课时间
const [actualSignNum, setActualSignNum] = useState(); // 实到人数
const [allStudentNum, setAllStudentNum] = useState(); // 应到人数
const [signRate, setSignRate] = useState(); // 到课比例
const [lateStudentNum, setLateStudentNum] = useState(); // 迟到人数
const [lateRate, setLateRate] = useState(); // 迟到比例
const [allConsumeNum, setAllConsumeNum] = useState(); // 扣课时应到
const [actualConsumeNum, setActualConsumeNum] = useState(); // 扣课时实到
const [consumeClassHour, setConsumeClassHour] = useState(); // 扣课时数
const [allNoneHourNum, setAllNoneHourNum] = useState(); // 不扣课时总人数
const [actualNoneHourNum, setActualNoneHourNum] = useState(); // 不扣课时实到人数
const [questionCount, setQuestionCount] = useState(); // 题目数
const [correctRate, setCorrectRate] = useState(); // 正确率
const [studentAnswerCount, setStudentAnswerCount] = useState(); // 答题人次
const [joinQuestionRate, setJoinQuestionRate] = useState(); // 答题参与率
const [trophyCount, setTrophyCount] = useState(); // 总共发放奖杯数
const [trophyStudentCount, setTrophyStudentCount] = useState(); // 获得奖杯学生数
const [raiseHand, setRaiseHand] = useState(); // 举手数
const [upPodiumCount, setUpPodiumCount] = useState(); // 上台数
const [allIntegralNum, setAllIntegralNum] = useState(); // 总发放积分数
const [integralStudentNum, setIntegralStudentNum] = useState(); // 积分获取人数
const [signIntegralNum, setSignIntegralNum] = useState(); // 到课发放积分数
const [signIntegralStudentNum, setSignIntegralStudentNum] = useState(); // 到课获得积分人数
const [interactiveIntegralNum, setInteractiveIntegralNum] = useState(); // 互动发放积分数
const [interactiveIntegralStudentNum, setInteractiveIntegralStudentNum] = useState(); // 互动获得积分人数
const [consumeClassTime, setConsumeClassTime] = useState(); // 互动获得积分人数
const [consumeHourNum, setConsumeHourNum] = useState(); // 扣课的课时数
const [loading, setLoading] = useState(true); // 扣课的课时数
useEffect(() => {
var animation = Lottie.loadAnimation({
path: "https://image.xiaomaiketang.com/xm/z32ddE2hNw.json",
name: "test",
renderer: "svg",
loop: true,
autoplay: true,
container: document.getElementById("lottie-box")
});
// 客户端调用客服去掉
if(!props.courseId){
const kefu = document.getElementsByClassName("zhiCustomBtn")[0];
kefu.style.display = "none";
}
let courseId = props.courseId || window.location.href.slice(window.location.href.indexOf("?")).split("=")[1];
axios.Apollo('anon/businessLive/getClassReport',{courseId}).then((res) => {
const data = res.result || {};
if (data.teacherName) {
setLoading(false);
}
let {
teacherName,
courseName,
liveType,
classTime,
actualStartTime,
actualEndTime,
actualSignNum,
allStudentNum,
signRate,
lateStudentNum,
lateRate,
allConsumeNum,
actualConsumeNum,
consumeClassHour,
allNoneHourNum,
actualNoneHourNum,
questionCount,
correctRate,
studentAnswerCount,
joinQuestionRate,
trophyCount,
trophyStudentCount,
raiseHand,
upPodiumCount,
allIntegralNum,
integralStudentNum,
signIntegralNum,
signIntegralStudentNum,
interactiveIntegralNum,
interactiveIntegralStudentNum,
consumeClassTime,
consumeHourNum
} = res.result;
setLiveType(liveType);
setTeacherName(teacherName);
setCourseName(courseName);
setActualStartTime(actualStartTime);
setActualEndTime(actualEndTime);
setClassTime(classTime);
setActualSignNum(actualSignNum);
setAllStudentNum(allStudentNum);
setSignRate(signRate);
setLateStudentNum(lateStudentNum);
setLateRate(lateRate);
setAllConsumeNum(allConsumeNum);
setActualConsumeNum(actualConsumeNum);
setConsumeClassHour(consumeClassHour);
setAllNoneHourNum(allNoneHourNum);
setActualNoneHourNum(actualNoneHourNum);
setQuestionCount(questionCount);
setCorrectRate(correctRate);
setStudentAnswerCount(studentAnswerCount);
setJoinQuestionRate(joinQuestionRate);
setTrophyCount(trophyCount);
setTrophyStudentCount(trophyStudentCount);
setRaiseHand(raiseHand);
setUpPodiumCount(upPodiumCount);
setAllIntegralNum(allIntegralNum);
setIntegralStudentNum(integralStudentNum);
setSignIntegralNum(signIntegralNum);
setSignIntegralStudentNum(signIntegralStudentNum);
setInteractiveIntegralNum(interactiveIntegralNum);
setInteractiveIntegralStudentNum(interactiveIntegralStudentNum);
setConsumeClassTime(consumeClassTime);
setConsumeHourNum(consumeHourNum);
});
}, []);
const renderSignInTooltip = () => {
return (
<div>
学员上课总时长达到<span className="class-high-light">{consumeClassTime}</span>分钟,即视为学员“到课”
</div>
);
};
const renderLateTooltip = () => {
return (
<div>
老师点击开始上课后<span className="class-high-light">{' 3 '}</span>分钟之后,才进入课堂的到课学员
</div>
);
};
const renderTotalTooltip = () => {
return (
<div>
“到课”学员扣<span className="class-high-light">{consumeHourNum}</span>课时
</div>
);
};
return (
<div className="teacher-class-report-page">
<div className="teacher-title">
<div className="teacher-name">{teacherName}</div><div className="text">老师,工作辛苦了,这是本节课的课堂报告~</div>
</div>
<div className="class-detail">
<div className="class-title">{courseName}</div>
<div className="class-duration">{`${moment(actualEndTime).format('YYYY')}年${moment(
actualEndTime,
).format('MM')}月${moment(actualEndTime).format('D')}日(${moment(actualEndTime).format(
'dddd',
)}) ${moment(actualStartTime).format('HH:mm')}-${moment(actualEndTime).format(
'HH:mm',
)} (${classTime}分钟)`}</div>
</div>
<div className="sign-in-detail">
<div className="title">到课情况</div>
<div className="content">
<div className="rate-block">
<div className="item">
<div className="rate">
{actualSignNum}
<span className="tiny">/{allStudentNum}</span>
</div>
<div className="explaination">
实到/应到人数
<Tooltip title={renderSignInTooltip()} placement="bottom">
<i className="icon iconfont" style={{ marginLeft: '5px' }}>
&#xe6f0;
</i>
</Tooltip>
</div>
</div>
<div className="item">
<div className="rate">{signRate}%</div>
<div className="explaination">到课比例</div>
</div>
<div className="item">
<div className="rate">{lateStudentNum}</div>
<div className="explaination">
迟到人数
<Tooltip title={renderLateTooltip()} placement="bottom">
<i className="icon iconfont" style={{ marginLeft: '5px' }}>
&#xe6f0;
</i>
</Tooltip>
</div>
</div>
<div className="item">
<div className="rate">{lateRate}%</div>
<div className="explaination">迟到率</div>
</div>
</div>
<div className="progress-block">
{!!allConsumeNum && (
<div className="item">
<div className="tip">
<div className="text">
<span className="tip-title">扣课时</span>实到/应到人数
</div>
<div className="num">
<span className="left-num">{actualConsumeNum}</span>/{allConsumeNum}
</div>
</div>
<div className="progress">
<div className="up-progress" style={{width:`${(actualConsumeNum/allConsumeNum *100)}%`}}></div>
<div className="down-progress"></div>
</div>
</div>
)}
{!!allConsumeNum && (
<div className="total-class">
<div className="text">
<span className="tip-title">总扣除课时</span>(超上不计入)
<Tooltip title={renderTotalTooltip()} placement="bottom">
<i className="icon iconfont" style={{ marginLeft: '5px' }}>
&#xe6f0;
</i>
</Tooltip>
</div>{' '}
<div className="num">
<span className="left-num">{consumeClassHour}课时</span>
</div>
</div>
)}
{!!allNoneHourNum && (
<div className="item">
<div className="tip">
<div className="text">
<span className="tip-title">不扣课时</span>实到/应到人数
</div>
<div className="num">
<span className="left-num">{actualNoneHourNum}</span>/{allNoneHourNum}
</div>
</div>
<div className="progress">
<div className="up-progress" style={{width:`${(actualNoneHourNum/allNoneHourNum *100)}%`}}></div>
<div className="down-progress"></div>
</div>
</div>
)}
</div>
</div>
</div>
<div className="class-data-detail">
<div className="title">课堂数据</div>
<div className="content">
<div className="data-block">
<div className="item">
<img src="https://image.xiaomaiketang.com/xm/zyDZaSMYSH.png" alt="" />
<div className="content">
<div className="one">
共发起了<span className="large-line-height">{questionCount}</span>
次答题,平均正确率达
<span className="large-line-height">{correctRate}%</span>
</div>
<div className="two">
参与答题人次<span className="line-height">{studentAnswerCount}</span>,参与率
<span className="line-height">{joinQuestionRate}%</span>(仅计算到课学员)
</div>
</div>
</div>
{(liveType === 'LARGE_CLASS_INTERACTION' || liveType === "SMALL_CLASS_INTERACTION") && (
<div className="item">
<img src="https://image.xiaomaiketang.com/xm/QXyjKpBrGX.png" alt="" />
<div className="content">
<div className="one">
共发放了<span className="large-line-height">{trophyCount}</span>个奖杯
</div>
<div className="two">
共有<span className="line-height">{trophyStudentCount}</span>个学生获得了奖杯
</div>
</div>
</div>
)}
{(liveType === 'LARGE_CLASS_INTERACTION' || liveType === "SMALL_CLASS_INTERACTION") && (
<div className="item column-center">
<img src="https://image.xiaomaiketang.com/xm/YrkH82fh86.png" alt="" />
<div className="content">
<div className="two">
共计举手人次<span className="line-height">{raiseHand}</span> {liveType === "LARGE_CLASS_INTERACTION" && <span>,上台人次
<span className="line-height">{upPodiumCount}</span></span>}
</div>
</div>
</div>
)}
<div className="item">
<img src="https://image.xiaomaiketang.com/xm/TieCG8wkyn.png" alt="" />
<div className="content">
<div className="one">
共计发放积分<span className="large-line-height">{allIntegralNum}</span>,有
<span className="large-line-height">{integralStudentNum}</span>人获得了积分
</div>
<div className="two">
通过“到课”发放积分<span className="line-height">{signIntegralNum}</span>,有
<span className="line-height">{signIntegralStudentNum}</span>人获得
</div>
<div className="three">
通过“互动”发放积分<span className="line-height">{interactiveIntegralNum}</span>
,有<span className="line-height">{interactiveIntegralStudentNum}</span>人获得
</div>
</div>
</div>
</div>
</div>
</div>
{ loading && <div className="class-loading-page">
<div className="loading-box">
<div id="lottie-box"></div>
<span className="box-tip">正在努力生成报告中...</span>
</div>
</div>}
</div>
);
};
export default TeacherClassReportPage;
.teacher-class-report-page {
.class-loading-page {
position: absolute;
z-index: 1;
left: 20px;
top: 60px;
right: 20px;
bottom: 0;
background: #fff;
#lottie-box {
width: 88px;
height: 106px;
margin: 0 auto;
}
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
.app-logo {
display: block;
margin: 0 auto;
width: 240px;
height: 240px;
}
.h5-logo {
display: block;
width: 220px;
height: 220px;
}
.box-tip {
margin-top: 16px;
display: block;
color: #666;
font-size: 26px;
.tip-button {
font-size: 26px;
color: #ffb100;
margin: 0 8px;
}
}
}
}
width: 512px;
text-align: center;
background-color: #fff;
.iconfont {
color: rgba(191, 191, 191, 1);
}
.teacher-title {
width: 512px;
background: #fff0e7;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
color: #666666;
line-height: 44px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
display: flex;
justify-content: center;
.teacher-name {
font-size: 16px;
font-weight: 400;
color: #ff7519;
line-height: 44px;
max-width: 192px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
.class-detail {
margin-top: 20px;
margin-bottom: 21px;
.class-title {
width: 480px;
margin: 0 auto;
font-size: 16px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #242b33;
line-height: 22px;
text-align: center;
}
.class-duration {
text-align: center;
height: 22px;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #8c8e93;
line-height: 22px;
margin-top: 8px;
}
}
.sign-in-detail {
.title {
height: 24px;
font-size: 17px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 24px;
text-align: center;
position: relative;
margin-bottom: 29px;
&::before {
content: '';
width: 24px;
height: 4px;
background: #fc9c6b;
border-radius: 8px;
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
}
}
.content {
.line-height {
color: rgba(255, 117, 25, 1);
}
.rate-block {
display: flex;
justify-content: space-between;
flex-flow: wrap;
.item {
width: 50%;
margin-bottom: 20px;
.rate {
height: 33px;
font-size: 24px;
font-weight: 500;
color: #333333;
line-height: 33px;
.tiny {
font-size: 18px;
}
}
.explaination {
text-align: center;
height: 20px;
font-size: 14px;
font-weight: 400;
color: #999999;
line-height: 20px;
}
}
}
.progress-block {
.item {
.tip {
display: flex;
justify-content: space-between;
.text {
height: 22px;
font-size: 14px;
color: rgba(140, 142, 147, 1);
line-height: 22px;
.tip-title {
color: rgba(51, 51, 51, 1);
font-weight: 500;
}
}
.num {
color: #333333;
line-height: 22px;
.left-num {
color: rgba(51, 51, 51, 1);
font-weight: 500;
font-size: 24px;
}
}
}
.progress {
position: relative;
width: 512px;
.up-progress {
position: absolute;
z-index: 10;
width: 80%;
height: 10px;
border-radius: 5px;
background-color: rgba(252, 156, 107, 1);
}
.down-progress {
width: 512px;
height: 10px;
background: #f6f7f8;
border-radius: 5px;
}
}
}
.total-class {
width: 512px;
height: 44px;
background: #f6f7f8;
border-radius: 2px;
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 20px;
line-height: 44px;
padding-left: 10px;
padding-right: 10px;
.text {
}
.num {
.left-num {
color: rgba(51, 51, 51, 1);
font-weight: 500;
font-size: 16px;
}
}
}
}
}
}
.class-data-detail {
margin-top: 30px;
.title {
height: 24px;
font-size: 17px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #333333;
line-height: 24px;
text-align: center;
position: relative;
margin-bottom: 11px;
&::before {
content: '';
width: 24px;
height: 4px;
background: #fc9c6b;
border-radius: 8px;
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
}
}
.content {
.data-block {
.item {
width: 512px;
padding: 26px 20px;
display: flex;
justify-content: flex-start;
text-align: left;
border-bottom: 1px dashed rgba(232, 232, 232, 1);
&:last-child {
border-bottom: none;
}
&.column-center {
align-items: center;
}
img {
display: inline-block;
width: 40px;
height: 42px;
margin-right: 16px;
}
.large-line-height {
color: rgba(255, 117, 25, 1);
font-size: 24px;
line-height: 22px;
}
.line-height {
color: rgba(255, 117, 25, 1);
font-size: 14px;
line-height: 22px;
}
}
}
}
}
}
.class-high-light {
color:rgba(255, 117, 25, 1);
margin-left: 4px;
margin-right: 4px;
}
.live-course-opt {
display: flex;
align-items: center;
position: relative;
margin-top: 4px;
.opt__right {
margin-left: 12px;
color: #FF8534;
}
.ant-btn {
margin-right: 12px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-05-19 11:01:31
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-05-25 16:50:47
* @Description 余额异常弹窗
*/
import React from 'react';
import { Modal } from 'antd';
import AccountChargeModal from './AccountChargeModal';
import './AbnormalModal.less';
class AbnormalModal extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
handleRecharge = () => {
const chargeModal = (
<AccountChargeModal
close={() => {
this.setState({
chargeModal: null,
});
this.props.onClose()
}}
refresh={() => { window.location.reload(); }}
/>
);
this.setState({ chargeModal });
};
render() {
const {
title, onClose, balance = 0, outDays, hasOwedFee
} = this.props;
const content = hasOwedFee ? (
<div className="content">
你的云课堂账户余额已持续欠费 <span className="high-light">{outDays}</span> 天,请及时充值。持续欠费 <span className="high-light">90</span> 天,系统将自动清除系统数据与回放视频。
</div>
) : (
<div
className="content"> 你的小麦云课堂余额不足(<span className="high-light">{balance.toFixed(2)}</span>元),为保障您直播间的正常使用,请您及时充值。余额耗尽后将无法进行直播,学员无法观看回放视频。
</div>
);
return (
<div className="abnormal-modal-wrapper">
<Modal
title={title}
visible={true}
footer={null}
onCancel={onClose}
className="abnormal-modal"
>
{ content }
<div
className="charge-btn"
onClick={() => { this.handleRecharge() }}
>去充值</div>
</Modal>
{ this.state.chargeModal }
</div>
)
}
}
export default AbnormalModal;
\ No newline at end of file
.abnormal-modal {
.high-light {
color: #EC4B35
}
.charge-btn {
width: 136px;
height: 40px;
line-height: 40px;
margin: auto;
margin-top: 40px;
text-align: center;
font-size: 16px;
color: #FFF;
background-color: #FC9C6B;
cursor: pointer;
}
}
\ No newline at end of file
/*
* @Description: 云课堂充值
* @Author: zhangyi
* @Date: 2020-05-09 15:02:39
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:55:25
*/
import React from 'react';
import { Modal, message, Form, InputNumber, Row, Col, Button } from "antd";
import { QuestionCircleOutlined } from '@ant-design/icons';
import qrcode from "@/libs/qrcode/qrcode.js";
import classNames from "classnames";
import AccountArgeement from "./ChargeArgeement";
import "./AccountChargeModal.less";
const FormItem = Form.Item;
const layout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
const buttonStyle = {
width: 302,
marginTop: 30,
height: 40,
marginLeft: 4,
lineHeight: "40px",
};
const ChannelType = {
WE_CHAT: "wechatPay",
ALI_PAY: "aliPay",
};
const limitNumber = (value) => {
return value.replace(/^(0+)|[^\d]+/g, "");
};
class AccountChargeModal extends React.Component {
constructor(props) {
super(props);
this.state = {
payType:
props.payInfo && props.payInfo.orderState == "PAY_UNDO"
? ChannelType[props.payInfo.payChannelType]
: "wechatPay",
orderId: null,
assetsId: null,
balance: 0,
payStatus: 0,
step: props.payInfo && props.payInfo.orderState == "PAY_UNDO" ? 2 : 1,
amount:
props.payInfo && props.payInfo.orderState == "PAY_UNDO"
? props.payInfo.rechargeBalance
: null,
amoutHelp: "最少充值500元",
validateStatus: null,
};
}
componentDidMount() {
// 发请求
if (!this.props.notQuery) {
this.getUserAssetsId();
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
//选择支付方式
changePayType = (type) => {
this.setState({
payType: type,
});
};
//查询账户Id
getUserAssetsId = () => {
window.axios
.Business("public/liveAssets/query", {
// instId: window.currentUserInstInfo.instId,
})
.then((res) => {
// const { teacherId, adminId } = window.currentUserInstInfo;
const { assetsId = "", balance } = res.result;
this.setState({
assetsId,
balance,
});
if (this.props.payInfo && this.props.payInfo.orderState == "PAY_UNDO") {
//继续充值的情况
const { orderId } = this.props.payInfo;
const params = {
orderId,
operatorId: !!teacherId ? teacherId : adminId,
};
this.reChargeSubmit("public/liveAssets/continueRecharge", params);
}
});
};
handleReChareg = _.debounce(
() => {
const { amount, payType, assetsId } = this.state;
if (!amount) {
this.setState({
amoutHelp: "请输入充值金额",
validateStatus: "error",
});
return;
}
axios
.Business("public/liveAssets/defaultRechargeAmount", {
// instId: window.currentUserInstInfo.instId,
})
.then((res) => {
const { assetsState, defaultAmount } = res.result;
if (amount && amount < 500 && assetsState == 1) {
//首充值
this.setState({
amoutHelp: "首次最少充值500元",
validateStatus: "error",
});
return;
}
if (
amount &&
amount < 500 &&
(assetsState == 2 || assetsState == 3)
) {
//不首充,不欠费
this.setState({
amoutHelp: "最少充值500元",
validateStatus: "error",
});
return;
}
if (amount && amount < defaultAmount && assetsState == 3) {
//不首充,欠费
this.setState({
amoutHelp: "最少充值需超过欠费金额",
validateStatus: "error",
});
return;
}
if (amount && amount > 20000) {
this.setState({
amount: 20000,
});
}
// const { instId, adminId, teacherId } = window.currentUserInstInfo;
const params = {
instId,
assetsId,
paymentType: payType == "wechatPay" ? "WECHAT" : "ALIPAY",
rechargeAmount: amount > 20000 ? 20000 : amount,
operatorId: !!teacherId ? teacherId : adminId,
reqSn: parseInt(Math.random() * Math.pow(10, 16)),
};
this.reChargeSubmit("public/liveAssets/recharge", params);
});
},
1000,
true
);
reChargeSubmit = (url, params) => {
axios.Business(url, params).then((res) => {
const data = res.result;
this.setState(
{
orderId: data.orderId,
step: 2,
payStatus: 0,
},
() => {
this.getQrcode(data);
this.cutDownTimer();
}
);
});
};
//获取二维码
getQrcode = (payInfo) => {
const text = payInfo.qrUrl;
setTimeout(() => {
const qrcodeNode = new qrcode({
text,
size: 150,
});
if (!document.querySelector("#qrcode").innerHTML) {
document.querySelector("#qrcode").appendChild(qrcodeNode);
}
});
};
cutDownTimer = () => {
this.timer = setInterval(() => {
this.getDealStatus(true);
}, 5000);
};
//获取订单状态
getDealStatus = (flag) => {
axios
.Business("public/liveAssets/rechargeState", {
orderId: this.state.orderId,
})
.then((res) => {
if (res.result == "PAY_SUCCESS") {
// 2是已支付
$(".qr-code-row #qrcode").innerHTML = "";
this.setState({ payStatus: 1 });
clearInterval(this.timer);
} else {
if (!flag) {
message.info("支付未完成");
}
return;
}
});
};
//关闭弹窗
handleCancel = () => {
const { step, payStatus } = this.state;
if (step == 2 && !payStatus) {
Modal.confirm({
title: "确定要放弃支付?",
content: `你的订单在30分钟内未支付将被取消,请尽快完成支付。`,
okText: "确认离开",
cancelText: "继续支付",
icon: <QuestionCircleOutlined />,
onCancel: () => {},
onOk: () => {
clearInterval(this.timer);
this.handleFresh();
},
});
} else {
if(payStatus == 1) {
this.props.refresh();
}
clearInterval(this.timer);
this.props.close();
}
};
//打开协议弹窗
handleToAgreement = () => {
const agreement = (
// <AccountArgeement
// close={() => {
// this.setState({
// agreement: null,
// });
// }}
// />
ge);
this.setState({
agreement,
});
};
handleChangeAmount = (val) => {
val = val > 20000 ? 20000 : val;
this.setState({
amount: val ? parseInt(val) : null,
amoutHelp: "最少充值500元",
validateStatus: null,
});
};
//关闭弹窗并刷新页面
handleFresh = () => {
this.props.close();
this.props.refresh();
};
render() {
const {
payType,
payStatus,
step,
amount,
validateStatus,
amoutHelp,
balance,
} = this.state;
const { getFieldDecorator } = this.props.form;
return (
<Modal
title="充值"
visible={true}
width={550}
footer={null}
className="account-charge-modal"
onCancel={() => {
this.handleCancel();
}}
>
<div>
{step == 1 && (
<div>
<Form style={{ width: 320, margin: "0 auto" }}>
<FormItem {...layout} label="当前余额:">
<span>{balance.toFixed(2)}</span>
</FormItem>
<FormItem
{...layout}
label="充值金额"
help={amoutHelp}
validateStatus={validateStatus}
>
<InputNumber
name="price"
style={{
width: 120,
borderColor: "#e8e8e8",
color: "#333",
}}
onChange={this.handleChangeAmount}
placeholder="请输入"
value={amount}
/>
<span style={{ marginLeft: 4 }}></span>
</FormItem>
<FormItem {...layout} label="支付方式:">
<Row type="flex" justify="start">
<Col>
<div
className={classNames("wechat-pay pay-item", {
active: payType == "wechatPay",
})}
onClick={() => {
this.changePayType("wechatPay");
}}
>
<span className="pay-radio-inner"></span>
<span className="icon-weixin icon iconfont">
&#xe659;
</span>
<span>微信</span>
</div>
</Col>
<Col>
<div
className={classNames("ali-pay pay-item", {
active: payType == "aliPay",
})}
onClick={() => {
this.changePayType("aliPay");
}}
>
<span className="pay-radio-inner"></span>
<span className="icon-ali icon iconfont">&#xe685;</span>
<span>支付宝</span>
</div>
</Col>
</Row>
</FormItem>
<FormItem>
<Button
type="primary"
style={buttonStyle}
onClick={this.handleReChareg}
>
充值
</Button>
<div className="charge-tips">
确认充值即表示已阅读并同意
<span
onClick={() => {
this.handleToAgreement();
}}
>
《服务协议》
</span>
</div>
</FormItem>
</Form>
</div>
)}
{step == 2 && (
<div>
{!payStatus ? (
<div className="pay-wrapper">
<div className="notice">
请用
<span
style={{
color: payType == "aliPay" ? "#58b7ef" : "#42ae3c",
}}
>
{payType == "aliPay" ? "支付宝" : "微信"}
</span>
扫一扫付款
</div>
<div className="money">{amount.toFixed(2)}</div>
<div className="qr-code" id="qrcode"></div>
</div>
) : (
<div className="pay-success">
<div className="img-wrap">
<span
className="icon iconfont"
style={{ fontSize: 60, color: "#5DD333" }}
>
&#xe800;
</span>
</div>
<p className="pay-success-name">充值成功</p>
<p className="pay-success-tip">
充值成功{amount.toFixed(2)}元,你可以到订单列表查看。
</p>
<Button type="primary" onClick={this.handleFresh}>
我知道了
</Button>
</div>
)}
</div>
)}
</div>
{this.state.agreement}
</Modal>
);
}
}
export default AccountChargeModal;
.account-charge-modal {
.pay-item {
border: 1px solid #e8e8e8;
padding: 10px 12px;
min-width: 112px;
display: flex;
justify-items: flex-start;
align-items: center;
font-size: 14px;
font-weight: 400;
color: rgba(51, 51, 51, 1);
line-height: 20px;
border-radius: 4px;
cursor: pointer;
.icon-weixin {
color: #42ae3c;
margin-right: 4px;
}
.icon-ali {
color: #58b7ef;
margin-right: 4px;
}
.pay-radio-inner {
display: inline-block;
width: 16px;
height: 16px;
border: 1px solid #eeeeee;
border-radius: 100%;
position: relative;
margin-right: 8px;
}
&.ali-pay {
margin-left: 8px;
}
&.active {
.pay-radio-inner {
border-color: #fc9c6b;
&::after {
content: " ";
display: inline-block;
width: 10px;
height: 10px;
background-color: #fc9c6b;
border-radius: 100%;
position: absolute;
top: 3px;
left: 3px;
}
}
}
}
.charge-tips {
margin-top: 10px;
font-size: 14px;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
text-align: center;
span {
color: #fc9c6b;
cursor: pointer;
}
}
.pay-success {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.pay-success-name {
color: #333333;
}
.pay-success-tip {
color: #666666;
margin-bottom: 40px;
margin-top: 8px;
}
}
.pay-wrapper {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
.money {
font-size: 24px;
font-weight: 500;
color: rgba(255, 133, 52, 1);
line-height: 33px;
margin-top: 8px;
margin-bottom: 16px;
}
}
}
.charge-explain-modal {
.explain-title {
font-size: 14px;
margin-bottom: 8px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 20px;
}
.other-explain {
font-size: 14px;
border-top: 1px solid #e8e8e8;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
padding-top: 15px;
margin-top: 16px;
}
.main-explain-text {
font-size: 14px;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
margin-left: 21px;
}
.main-explain-block {
width: 816px;
height: 32px;
line-height: 32px;
background: rgba(247, 248, 249, 1);
border-radius: 4px;
padding-left: 16px;
margin-left: 21px;
margin-bottom: 8px;
}
ul {
margin-bottom: 8px;
margin-top: 8px;
}
ul > li {
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: rgba(102, 102, 102, 1);
line-height: 20px;
margin-bottom: 0;
margin-left: 21px;
display: flex;
justify-self: flex-start;
align-items: center;
.spot {
width: 4px;
height: 4px;
border-radius: 100%;
background-color: #666666;
margin-right: 10px;
}
}
table {
border: 1px solid #e8e8e8;
margin-left: 33px;
margin-bottom: 16px;
tr > th,
tr > td {
height: 40px;
padding-left: 24px;
border-right: 1px solid #e8e8e8;
border-bottom: 1px solid #e8e8e8;
}
}
}
.charging-detail-modal {
.detail-title {
font-size: 16px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 22px;
margin-bottom: 18px;
}
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
}
.charge-agreement-modal{
.agreement-title {
text-align: center;
font-size: 24px;
line-height: 30px;
color: #333333;
font-weight: bold;
margin-bottom: 30px;
}
.agreement-sub-title {
margin-top: 16px;
margin-bottom: 8px;
font-size: 16px;
line-height: 22px;
color: #333333;
font-weight: bold;
}
.agreement-main {
font-size: 15px;
line-height: 21px;
color: #666;
margin-top: 8px;
text-indent: 16px;
}
.agreement-main-text {
font-size: 15px;
line-height: 21px;
color: #666;
text-indent: 30px;
margin-top: 4px;
}
.agreement-tip {
margin-left: 24px;
}
li {
margin-bottom: 4px;
}
.border-all {
padding: 16px 0;
border-top: 1px solid #eeeeee;
border-bottom: 1px solid #eeeeee;
margin-top: 16px;
margin-bottom: 16px;
}
}
\ No newline at end of file
import React from 'react';
import { Modal, Table } from "antd";
import ChargeArgeement from "./ChargeArgeement";
import "./AccountChargeModal.less";
class AccountChargeRecords extends React.Component{
constructor(props) {
super(props);
this.state = {
list: [],
};
}
componentDidMount() {
this.getList();
}
getList = () => {
const { instId } = window.currentUserInstInfo;
axios
.Business("public/liveAssets/rechargeProtocol", { instId })
.then((res) => {
const list = res.result;
this.setState({
list,
});
});
};
handleProtcol = (id) => {
const agreement = (
<ChargeArgeement
id={id}
close={() => {
this.setState({
agreement: null,
});
}}
/>
);
this.setState({
agreement,
});
};
render() {
const columns = [
{
title: "签订人",
dataIndex: "operatorName",
width: 140
},
{ title: "关联订单ID", dataIndex: "orderId" },
{
title: "签订时间",
dataIndex: "createTime",
render: (text, record) => {
return <span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>;
},
},
{
title: "签订协议",
dataIndex: "operate",
render: (text, record) => {
return (
<div
style={{ cursor: "pointer", color: "#FC9C6B" }}
onClick={() => {
this.handleProtcol(record.protocolId);
}}
>
《服务协议》
</div>
);
},
},
];
const { list } = this.state;
return (
<Modal
title="服务协议签订记录"
visible={true}
width={680}
footer={null}
onCancel={() => {
this.props.close();
}}
>
<div>
<div
style={{
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
marginBottom: 16,
}}
>
以下是本校区自助充值时签订协议的记录
</div>
<Table
size="middle"
columns={columns}
dataSource={list}
pagination={false}
bordered
/>
</div>
{this.state.agreement}
</Modal>
);
}
}
export default AccountChargeRecords;
import React from 'react';
import { Modal } from "antd";
import "./AccountChargeModal.less";
class ChargeArgeement extends React.Component {
constructor(props) {
super(props);
this.state = {
capitalTotalAmount: "",
totalAmount: "",
buyerPhone: "",
buyer: "",
};
}
componentDidMount() {
this.getProtcolContent();
}
getProtcolContent() {
const { id } = this.props;
const { instId } = window.currentUserInstInfo;
if (!!id) {
const params = { protocolId: id, instId };
axios
.Business("public/liveAssets/protocolDetail", params)
.then((res) => {
const {
capitalTotalAmount,
totalAmount,
buyerPhone,
buyer,
} = res.result;
this.setState({
capitalTotalAmount,
totalAmount,
buyerPhone,
buyer,
});
});
}
}
render() {
return (
<Modal
title="云课堂充值服务协议"
visible={true}
width={800}
className="charge-agreement-modal"
footer={null}
onCancel={() => {
this.props.close();
}}
>
<div>
<div className="agreement-title">
“互动班课”技术服务许可及服务协议”
</div>
<div className="agreement-main">
<span>软件采购方:{this.state.buyer}</span>
</div>
<div className="agreement-main">
<span>联系方式:{this.state.buyerPhone}</span>{" "}
<span style={{marginLeft: 300}}>(以下简称 “甲方” )</span>
</div>
<div className="agreement-main-text border-all">
<p> 授权许可人:杭州杰竞科技有限公司 (以下简称 “乙方” ) </p>
<p>地址:杭州市西湖区古墩路598号同人广场A座三楼 </p>
<p>邮编:310012</p>
<p>联系电话:0571-87007093</p>
</div>
<div className="agreement-main-text">
“互动班课”技术服务是乙方自主设计研发的用于服务教育机构在线课堂的工具类产品。因从事教育相关的经营活动以及自身管理需要,甲方向乙方购买“互动班课”技术服务的相关使用权,双方在真实、充分地表达各自意愿的基础上,根据《中华人民共和国合同法》和相关法律法规的规定,特订立以下合同条款:
</div>
<p className="agreement-sub-title">一、授权使用内容</p>
<div className="agreement-main-text">
乙方授权使用的“互动班课”技术服务包含以下内容:互动班课直播工具、充值账户等服务。
</div>
<div className="agreement-main-text">
乙方许可甲方在本合同约定期限内使用“互动班课”技术服务包括了基于PC端的操作系统、基于PC客户端的上课操作系统、基于手机APP的家长端上课操作系统,以及未来计划基于手机H5端的家长操作环境;同时乙方负责甲方在使用“互动班课”技术服务的期限内,提供必要的培训、指导、帮助和技术支持(包含:产品布设、使用培训、数据处理、后期维护等)。
</div>
<p className="agreement-sub-title">二、计价标准与付款方式</p>
<div className="agreement-main-text">
1、“互动班课”在线课堂计价内容和计价标准如下:
<br />
<span className="agreement-tip">
计价内容:直播课时费、录制费。
</span>
<span className="agreement-tip">
其中,直播课时费依据直播间上课师生人数、直播教室类型、直播时长收取;录制费依据回放视频时长收取。
</span>
<br />
<span className="agreement-tip"> 计价标准:</span>
<br />
<span className="agreement-tip">
【直播时长费】计算公式:每节课上课费用=排课时长*计费人数*课时单价(元/小时)
</span>
<br />
<span className="agreement-tip">
排课时长:按小时计算,0.5h起,不足0.5h的按0.5h计算
</span>
<br />
<span className="agreement-tip">
{"计费人数:累积在线时长>=10min的上课老师、学生、助教人数总和"}
</span>
<br />
<span className="agreement-tip">
课时单价:不同直播教室类型的课时单价(元/人/小时)不同,分别如下
</span>
<br />
<br />
<span className="agreement-tip">1人上台:3元/人/小时</span>
<br />
<span className="agreement-tip">2-6人上台:3元/人/小时</span>
<br />
<span className="agreement-tip">7-8人上台:4元/人/小时</span>
<br />
<span className="agreement-tip">9-12人上台:8元/人/小时</span>
<br />
<span className="agreement-tip">
【录制费】计算公式:回放视频时长*单价(2元/小时)
</span>
<br />
<span className="agreement-tip">
录制费依据回放视频生成后的回放视频时长进行计算,回放视频时长按0.5h起收,不足0.5h按0.5h结算。
</span>
<br />
</div>
<div className="agreement-main-text">
2、以下为用户在 “互动班课”后台可选的增值服务:
<li>
(1)云端录课、网页直播和网页回放功能:云端录课、网页直播和网页回放功能:乙方对云端录课功能按甲方实际录课视频的时长进行收费(2元/小时),最小计费单位为分钟,收费时长以计算课节消费的时长为上限。网页直播和网页回放,暂时免费。开通收费将提前30天通知甲方。
</li>
<li>
(2)结算方式:选择“互动班课”在线课堂服务需要预存【500】元课时费,收费系统按实际发生的课时使用数扣费。合同到期续签时不用再预存上述费用,账户余额可继续使用。为不影响正常上课,甲方应在余额耗尽前及时续费,余额不足时将限制甲方的功能使用。因余额不足导致甲方损失的应由甲方自行承担,每次续费金额应不低于【500】元。
甲方可在自己系统后台进行自主充值。如在本合同的期限内,甲方未按照前述约定续费,则乙方有权在甲方预存的费用使用完毕后,停止向甲方提供本合同项下的技术服务,并按照本合同的约定合同解除而无需承担任何责任。合同到期后,如甲方不再续签,但仍有账户余额,合同相关服务自动延续三个月,三个月后合同照常终止,此后如仍有余额合同照常终止,如需继续使用须进行续签。
</li>
<li>
(4)甲方可以在 b.xiaomai5.com 随时查询甲方的付费记录及课时使用清单。乙方在收到甲方支付的预存课时费或续费后,根据甲方发票申请信息开具发票。
</li>
</div>
<div className="agreement-main-text">
3、甲方本次购买“互动班课”技术服务明细如下:其中价格__元为赠送充值内容
</div>
<div className="agreement-main-text">
4、甲方本次购买技术服务金额合计为:__元,
</div>
<div className="agreement-main-text">
大写为:__元整。
</div>
<div className="agreement-main-text">
5、甲方确认开户校区系统账号为唯一云课堂充值账户,甲方可在开户、充值前进行核对。
</div>
<div className="agreement-main-text">
6、双方约定,乙方收到由甲方支付的相应款项起即时为其开通云课堂账户,若甲方指定的开户日期早于约定开户日期,则以甲方指定的开户日期为准。
</div>
<div className="agreement-main-text">
7、双方约定,乙方收到由甲方在线支付的相应款项起即时完成互动班课账户的充值,若甲方委托线下支付,则乙方在收到款项起第90天内为其完成互动班课账户的充值,若甲方指定的充值日期早于约定充值日期,则以甲方指定的充值日期为准。
</div>
<div></div>
<p className="agreement-sub-title">三、甲方的权利和义务</p>
<div className="agreement-main-text">
1、甲方有权要求乙方为其初始数据的整理与编辑工作提供指导。
</div>
<div className="agreement-main-text">
2、甲方有权要求乙方为甲方提供系统技术服务的操作培训。
</div>
<div className="agreement-main-text">
3、甲方需妥善保管相关充值账户账号密码,同时不得利用“互动班课”技术服务从事任何违法、违规活动。未经乙方书面同意,甲方不得另行转让、出借账号或者以其他方式变相给第三方使用,否则乙方有权关闭账号或要求甲方赔偿损失。甲方因内部员工误操作或恶意删除相关数据导致的数据丢失,乙方不负责进行恢复。
</div>
<div className="agreement-main-text">
4、甲方在使用“互动班课”技术服务过程中,必须遵循以下原则:
<li>
(1)甲方有权通过乙方提供的“互动班课”在线教室平台,自由使用该平台所提供的所有功能,并有权得到乙方相应的技术支持和服务,并遵守所有与网络服务有关的法律法规、行业规定以及协议约定。
</li>
<li>
(2)甲方必须遵守《计算机信息网络国际联网安全保护管理办法》,《中华人民共和国计算机信息网络国际联网管理暂行规定》,《中华人民共和国计算机信息系统安全保护条例》,《中华人民共和国电信条例》,《互联网信息服务管理办法》和国家其他有关法律、法规、条例,不得进行任何违法经营活动。甲方承诺其利用乙方提供的技术服务发布的信息、召开的会议或进行的任何经营行为不会违反上述规定,否则甲方应承担因违反上述规定而引起的法律责任,并对因此给乙方造成的经济、声誉损失承担赔偿责任。
</li>
<li>
(3)不得利用“互动班课”技术服务进行任何可能对互联网或移动网正常运转造成不利影响的行为;
</li>
<li>
(4)不得利用“互动班课”技术服务上传、展示或传播任何虚假的、骚扰性的、中伤他人的、辱骂性的、恐吓性的、庸俗淫秽的或其他任何非法信息资料;甲方若出现淫秽、违反宪法、法律明确规定的画面或言论而被第三方投诉,则乙方有权无条件终止本协议并要求甲方进行相应赔偿。
</li>
<li>
(5)不得侵犯其他任何第三方的专利权、著作权、商标权、名誉权或其他任何合法权益;
</li>
<li>
(6)不得利用“互动班课”技术服务进行任何不利于双方合作的行为。
</li>
<li>
(7)甲方使用在线教室平台及乙方提供的技术服务时所产生的包括但不限于课件、音频、视频等内容(下称“甲方衍生作品”)的知识产权全部归甲方所有。
</li>
</div>
<div className="agreement-main-text">
5、甲方违法或违反合同使用“互动班课”产生的后果乙方概不负责。甲方使用“互动班课”过程中因违反第三方的相关管理规定导致的后果由其自行承担。
</div>
<p className="agreement-sub-title">四、乙方的权利和义务</p>
<div className="agreement-main-text">
{" "}
1、乙方将为甲方提供在线课堂的操作培训;
</div>
<div className="agreement-main-text">
2、乙方将为甲方解决在线课堂使用中遇到的问题;
</div>
<div className="agreement-main-text">
3、乙方作为本合同所述产品的软件著作权人,负责“互动班课”的技术支持、产品升级、后台数据统计等事项;
</div>
<div className="agreement-main-text">
4、乙方负责“互动班课”技术服务的数据安全与技术运维服务;
</div>
<div className="agreement-main-text">
5、乙方配合甲方进行合理的品牌宣传、推广活动,具体执行方案需双方协商一致。
</div>
<p className="agreement-sub-title">五、知识产权</p>
<div className="agreement-main-text">
1、“互动班课”技术服务(包括:PC端后台管理系统、PC客户端、移动端APP等),均系乙方自主研发完成。“互动班课”技术服务涉及的著作权、商标权、专利权、商业秘密等全部知识产权,以及相关的信息内容,包括但不限于:文字表述及其组合、图标、图饰、图表、色彩、界面设计、版面框架、有关数据、印刷材料及电子文档等均受中华人民共和国著作权法、商标法、专利法、反不正当竞争法和相应的国际条约以及其他知识产权法律法规的保护,
除涉及第三方授权的软件或技术外,乙方享有上述全部知识产权及所有权。
</div>
<div className="agreement-main-text">
2、基于履行本合同之目的,甲方在合同有效期内享有不可转让的、非独占性的产品使用权。甲方不得进行任何反向编译或试图提取产品源代码的行为,不得创作任何版权衍生作品。
</div>
<div className="agreement-main-text">
3、甲方不得从事以任何形式损害乙方知识产权的行为,同时应采取有效措施促使其雇员按照本合同的规定维护乙方的知识产权。
</div>
<div className="agreement-main-text">
4、未经双方同意,双方均不得以商业经营为目的,擅自使用对方的商标、标识等商业信息。
</div>
<p className="agreement-sub-title">六、保密条款</p>
<div className="agreement-main-text">
1、在本合同的履行期,双方应当对另一方的商务、财务、技术、产品信息、用户资料或其他标明保密的文件保守秘密,未经信息利益方书面同意,另一方不得向任何第三方进行披露。但以下特定情形除外:
<br />
(1)根据法律法规规定或有权机关的指示提供;
<br />
(2)信息利益方自行向第三方公开其个人隐私信息;
<br />
(3)任何因黑客攻击、电脑病毒侵入及其他不可抗力事件导致信息的泄露。
</div>
<div>2、本保密条款在本合同期满、解除或终止后仍然有效</div>
<p className="agreement-sub-title">七.履约说明</p>
<div className="agreement-main-text">
软件实际使用人与甲方不一致的,甲方应向乙方说明软件的实际使用人,由乙方向其提供服务。
</div>
<p className="agreement-sub-title">八.不可抗力</p>
<div className="agreement-main-text">
遭受不可抗力事件的一方(该事件包括但不限于政府行为、自然灾害、战争、黑客袭击或其它类似事件)可暂行中止履行本合同项下的义务直至不可抗力事件的影响消除为止,并且无需为此承担违约责任;但应书面通知对方遭遇不可抗力的情况并尽最大努力克服该事件,减轻其负面影响。
</div>
<p className="agreement-sub-title">九.合同解除与违约责任</p>
<div className="agreement-main-text">
1、若甲方存在合同内包括侵犯知识产权、违反保密条款或违反网络产品使用原则等行为,乙方有权解除合同;若“互动班课”技术服务无法使用或乙方无合理理由拒绝维护程序或提供必要指导的,甲方有权解除合同。
</div>
<div className="agreement-main-text">
2、解除合同的一方可以向违约方主张损失。但解除合同的一方有将自身损失控制在最小的义务。
</div>
<p className="agreement-sub-title">十、争议处理</p>
<div className="agreement-main-text">
由本合同引起的或与本合同有关的任何争议,由双方友好协商解决。协商不成,双方同意提交乙方住所地有管辖权的人民法院诉讼解决。
</div>
<p className="agreement-sub-title">十一、合同有效期及合同变更</p>
<div className="agreement-main-text">
1、本合同有效期自双方签署之日起算,至“互动班课”充值账户到期日为止,甲方在合同有效期内方可按约定使用“互动班课”技术服务。
</div>
<div className="agreement-main-text">
2、本合同到期后将自动终止,若需延期,双方应按照乙方届时的费用标准另行签订协议。
</div>
<div className="agreement-main-text">
3、对本合同条款所做的任何变更以及其他未尽事宜,均须由双方协商解决,为此所形成的书面文件或补充附件等,与本合同具有同等法律效力
</div>
<p className="agreement-sub-title">十二.附加说明</p>
<div className="agreement-main-text">
1,甲乙双方所有权利义务都以本协议约定为准,乙方委派的销售人员如有与本协议内容不符的任何承诺均不代表乙方的真实意图和承诺。
</div>
<div className="agreement-main-text">
2,除本协议条款二第1,2,4条允许手写(仅限于条款规定的日期,价格,版本与名称等相关内容)外,其他位置均以打印字体为准。
</div>
</div>
</Modal>
);
}
}
export default ChargeArgeement;
/*
* @Description: 计费说明
* @Author: zhangyi
* @Date: 2020-05-09 15:21:22
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 15:14:28
*/
import { Modal, Button, Table } from "antd";
import "./AccountChargeModal";
const data = [
{ person: "上台人数1v1", price: "3元/人/小时" },
{ person: "上台人数1v2", price: "3元/人/小时" },
{ person: "上台人数1v3", price: "3元/人/小时" },
{ person: "上台人数1v4", price: "3元/人/小时" },
{ person: "上台人数1v5", price: "3元/人/小时" },
{ person: "上台人数1v6", price: "3元/人/小时" },
{ person: "上台人数1v7", price: "4元/人/小时" },
{ person: "上台人数1v8", price: "4元/人/小时" },
{ person: "上台人数1v9", price: "8元/人/小时" },
{ person: "上台人数1v10", price: "8元/人/小时" },
{ person: "上台人数1v11", price: "8元/人/小时" },
{ person: "上台人数1v12", price: "8元/人/小时" },
];
function ChargeExplainModal(props) {
return (
<Modal
title="计费说明"
visible={true}
className="charge-explain-modal"
width={880}
onCancel={() => {
props.close();
}}
footer={[
<Button
type="primary"
onClick={() => {
props.close();
}}
>
关闭
</Button>,
]}
>
<div>
<div className="explain-title">1)直播课时费</div>
<p className="main-explain-block">
每节课上课费用 = 上台人数单价 × 有效出勤学生和老师人数 × 排课时长
</p>
<ul>
<li>
1. 上课老师、学生和助教在教室中的累计时长满10分钟即为有效出勤;
</li>
<li>
2.
排课时长指创建课节设置的课节时长,最小计费单位为0.5小时,不足0.5小时按0.5小时计算;
</li>
<li>
3.
上台人数单价:以排课时设定的上台人数上限为准,不同直播教室类型的课时单价不同。
</li>
</ul>
<p className="main-explain-text" style={{marginTop: 16,marginBottom: 8}}>
温馨提醒:上台人数1vN,1为授课老师,N为同时与老师视频互动学生数;直播课时费将在老师下课后立即结算。
</p>
<table style={{ width: 333 }}>
<thead>
<tr>
<th>上台人数</th>
<th>价格标准</th>
</tr>
</thead>
<tbody>
{data.map((item) => {
return (
<tr>
<td>{item.person}</td>
<td>{item.price}</td>
</tr>
);
})}
</tbody>
</table>
<p className="main-explain-text">
示例:王老师排了一节45分钟的课程,选择的直播教室类型为5人上台,安排了1位助教、12位学生。那么,排课时长45分钟记为1小时;全员累计在线时长≥10min,老师+助教+学生共计1+1+12=14人;课时单价3元/人/小时。所以,此节课的直播课时费=1x14x3=42元
</p>
<div className="explain-title mt16">2)录制费</div>
<p className="main-explain-block">录制费 = 录课单价 × 回放视频时长</p>
<ul>
<li>1. 结算时间:从回放视频生成后立即结算</li>
<li> 2. 回放视频时长:0.5h起收,不足0.5h的按0.5h结算</li>
<li> 3. 单价:2元/小时</li>
</ul>
<p className="main-explain-text">
示例:生成了49分26秒的回放视频,不足1h按1h计算,总费用=1(h)*2元/h=2元
</p>
<div className="explain-title mt16">3)流量费</div>
<p className="main-explain-block">
观看回放视频流量费 = 流量单价 × 回放流量
</p>
<p className="main-explain-text">
目前收费报价:0元(限时免费)
<br /> 若后续变更收费,将提前30日通告收费方式及价格标准
</p>
<div className="explain-title mt16">4)存储费</div>
<p className="main-explain-block">存储实际扣费 = 存储单价 × 存储文件大小</p>
<p className="main-explain-text">
目前收费报价:0元(限时免费)
<br />
若后续变更收费,将提前30日通告收费方式及价格标准
</p>
<div className="explain-title mt16">5)其他说明</div>
<div className="main-explain-text">
余额不足时将限制使用创建直播课、老师和学生进入直播间、老师和学生观看回放视频、在资料云盘或直播间上传文件等功能,请注意及时充值。(余额<300元时将发送短信提醒,请注意查看。)
</div>
</div>
</Modal>
);
}
export default ChargeExplainModal;
import React from 'react';
import { Modal, Table, Tooltip } from "antd";
import { ShowTips, PageControl } from "@/components";
import "./AccountChargeModal.less";
class ChargingDetailModal extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
liveCourseId: props.liveCourseId,
},
totalCount: 0,
list: [],
teacherList: [],
};
}
componentDidMount() {
// 发请求
this.handleToPage();
this.getTeacherData();
}
handleToPage = (page = 1) => {
const params = _.clone(this.state.query);
params.current = page;
axios
.Apollo("public/businessLive/queryStudentVisitData", params)
.then((res) => {
if (res.result) {
const { records = [], total } = res.result;
this.setState({
list: records,
totalCount: total,
query: params,
});
}
});
};
getTeacherData = () => {
window.axios
.Apollo("public/businessLive/queryTeacherVisitData", {
liveCourseId: this.props.liveCourseId,
})
.then((res) => {
if (res.result) {
const teacherList = [res.result];
this.setState({
teacherList,
});
}
});
};
dealTimeDuration = (time) => {
const diff = Math.floor(time % 3600);
let hours = Math.floor(time / 3600);
let mins = Math.floor(diff / 60);
let seconds = Math.floor(time % 60);
hours = hours < 10 ? "0" + hours : hours;
mins = mins < 10 ? "0" + mins : mins;
seconds = seconds < 10 ? "0" + seconds : seconds;
return hours + ":" + mins + ":" + seconds;
};
getColumns = (type) => {
const columns = [
{
title: type == "student" ? "学生姓名" : "老师姓名",
dataIndex: "userName",
},
{
title: "手机号",
dataIndex: "phone",
render: (text, record) => {
return <p>{text}</p>;
},
},
{
title: "累计在线时长",
dataIndex: "totalDuration",
render: (text, record) => {
return <span>{text ? this.dealTimeDuration(text) : '-'}</span>;
},
},
{
title: (
<span>
是否计费&nbsp;
<Tooltip title="仅对累计在线时长≥10分钟的老师或学员计费">
<span className="icon iconfont">&#xe6f2;</span>
</Tooltip>
</span>
),
dataIndex: "type",
render: (text, record) => {
return <span>{record.totalDuration > 600 ? "计费" : "不计费"}</span>; //大于十分钟的计费
},
},
];
return columns;
};
render() {
const { list, query, totalCount, teacherList } = this.state;
return (
<Modal
title="计费人数详情"
visible={true}
width={680}
className="charging-detail-modal"
footer={null}
onCancel={() => {
this.props.close();
}}
>
<div>
<div style={{ marginBottom: 16 }}>
<div className="detail-title">老师详情</div>
<Table
size="middle"
columns={this.getColumns("teacher")}
dataSource={teacherList}
pagination={false}
bordered
/>
</div>
<div className="detail-title">学生详情</div>
<Table
size="middle"
columns={this.getColumns("student")}
dataSource={list}
pagination={false}
bordered
/>
<PageControl
size="small"
current={query.current - 1}
pageSize={query.size}
total={totalCount}
toPage={(page) => {
this.handleToPage(page + 1);
}}
/>
</div>
</Modal>
);
}
}
export default ChargingDetailModal;
/*
* @Description: 直播开始上课之前对余额的校验
* @Author: zhangyi
* @Date: 2020-05-18 13:47:42
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:36:33
*/
import React from 'react';
import { Modal, Button } from "antd";
import AccountChargeModal from "./AccountChargeModal";
const textStyle = {
lineHeight: "20px",
fontSize: "14px",
color: "#666666",
};
class CheckBalanceModal extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
handleReCharge = () => {
const chargeModal = (
<AccountChargeModal
close={() => {
this.setState({
chargeModal: null,
});
}}
refresh={() => {
this.props.close();
}}
/>
);
this.setState({
chargeModal,
});
};
render() {
const { balance, currentClass, undoClass } = this.props;
return (
<Modal
visible={true}
footer={null}
width={550}
title="余额不足"
onCancel={() => {
this.props.close();
}}
>
<div>
<div style={textStyle} className="mb8">
本次直播您将扣除{currentClass.toFixed(2)}元费用,预计当前账户余额不足抵扣本次课次及其他结算课次费用,请您尽快充值。
</div>
<div style={textStyle}>
当前余额:
<span style={{ color: "#EC4B35" }}>{balance.toFixed(2)}</span>
</div>
<div style={textStyle}>
本课次预估消费:<span>{currentClass.toFixed(2)}</span>
</div>
<div style={textStyle}>
其他未结算课次预估消费:<span>{undoClass.toFixed(2)}</span>
</div>
<div style={{ textAlign: "center", marginTop: 25 }}>
<Button
style={{
width:136,
height:'40px',
lineHeight: '40px',
borderRadius:'4px'
}}
type="primary"
onClick={() => {
this.handleReCharge();
}}
>
立即充值
</Button>
</div>
</div>
{this.state.chargeModal}
</Modal>
);
}
}
export default CheckBalanceModal;
/*
* @Description: 上课记录
* @Author: zhangyi
* @Date: 2020-05-12 09:43:48
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:22:42
*/
import React, { useState, useEffect } from "react";
import { Modal, Table, Input, Button, message, Checkbox, Tooltip } from "antd";
import Bus from '@/core/bus';
import { PageControl } from "@/components";
import hasExportPermission from '../utils/hasExportPermission';
import dealTimeDuration from '../utils/dealTimeDuration';
import "./ClassRecordModal.less";
const { Search } = Input;
class ClassRecordModal extends React.Component {
constructor(props) {
super(props);
this.state = {
teacherData: [],
classList: [],
query: {
current: 1,
size: 10,
nameOrPhone: "",
liveSignState: '',
durationSort: null,
liveCourseId: props.liveItem.liveCourseId,
},
};
}
componentDidMount() {
this.fetchClassList();
this.fetchTeacherData();
}
fetchClassList = (page = 1) => {
const params = _.clone(this.state.query);
if(!params.liveSignState) {
delete params.liveSignState;
}
params.current = page;
window.axios
.Apollo("public/businessLive/queryStudentVisitData", params)
.then((res) => {
if (res.result) {
const { records = [], total } = res.result;
this.setState({
classList: records,
total,
query: params,
});
}
});
};
fetchTeacherData = () => {
const { liveCourseId } = this.props.liveItem;
window.axios
.Apollo("public/businessLive/queryTeacherVisitData", { liveCourseId })
.then((res) => {
if (res.result) {
const teacherData = [res.result];
this.setState({
teacherData,
});
}
});
};
getColumns = (type) => {
const { consumeClassTime } = this.props.liveItem;
const source = this.props.type;
const columns = [
{
title: type == "student" ? "学生姓名" : "老师姓名",
dataIndex: "userName",
},
{
title: "手机号",
dataIndex: "phone",
render: (text, record) => {
return (
<p>
{!(
(!window.NewVersion && !window.currentUserInstInfo.teacherId) ||
(window.NewVersion && Permission.hasEduStudentPhone())
) && type == "student"
? (text || "").replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3")
: text}
</p>
);
},
},
{
title: type == "student" ? "观看直播次数" : "进入直播间次数",
dataIndex: "entryNum",
align:'right'
},
{
title: "累计上课时长",
dataIndex: type == "student" ? "watchDuration" : "totalDuration",
sorter:
type == "student"
? (a, b) => a.watchDuration - b.watchDuration
: null,
sortDirections: ["descend", "ascend"],
render: (text, record) => {
//如无离开时间,就置空
return (
<span>
{text ? dealTimeDuration(text) : '00:00:00'}
</span>
);
},
},
];
if(type == "student") {
columns.push({
title: <span>到课状态<Tooltip title={<div>学员累计上课时长达到<span className="bulge">{consumeClassTime}</span>分钟,即视为学员“到课”</div>}><span className="iconfont">&#xe6f2;</span></Tooltip></span>,
width: 100,
dataIndex: "signState",
render: (text) => {
if(text) {
return <span>{text === 'ABSENT' ? '未到' : '到课'}</span>
} else {
return <span>-</span>
}
}
})
if(source) {
columns.push({
title: "获得奖杯数",
dataIndex: "trophyNum",
align:'right',
render: (text) => {
return <span>{text ? text : 0}</span>
}
})
}
}
return columns;
};
handleTableChange = (pagination, filters, sorter) => {
const query = this.state.query;
if (!_.isEmpty(sorter)) {
if (sorter.columnKey === "totalDuration") {
if (sorter.order === "ascend") {
query.durationSort = "SORT_ASC";
} else if (sorter.order === "descend") {
query.durationSort = "SORT_DESC";
}
this.setState({ query }, this.fetchClassList);
}
}
};
// 5.0导出
handleExportV5 = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'public/businessLive/exportLargeClassLiveAsync' : 'public/businessLive/exportClassInteractionLiveSync'
window.axios.Apollo(url, {
liveCourseId,
exportLiveType: 'VISITOR'
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“任务中心”进行下载',
okText: '我知道了',
});
})
}
// 4.0导出
handleExport = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'api-b/b/lesson/exportLargeClassLiveAsync' : 'api-b/b/lesson/exportClassInteractionLiveSync';
window.axios.post(url, {
liveCourseId,
exportLiveType: 1
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“导出中心”进行下载',
okText: '我知道了',
});
})
}
render() {
const { type } = this.props;
const { query, total, teacherData, classList } = this.state;
const expandedColumns = [
{
title: "进入时间",
dataIndex: "entryTime",
key: "entryTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{
title: "离开时间",
dataIndex: "leaveTime",
key: "leaveTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{
title: "上课时长",
dataIndex: "lookingDuration",
key: "lookingDuration",
render: (text, record) => {
return <span>{text ? dealTimeDuration(text) : '-'}</span>;
},
},
];
return (
<Modal
title="上课记录"
visible={true}
footer={null}
width={680}
className="class-record-modal"
onCancel={() => {
this.props.close();
}}
>
<div>
<p className="class-record-title" style={{ marginBottom: 18 }}>
老师上课数据
</p>
<Table
size="small"
columns={this.getColumns("teacher")}
dataSource={teacherData}
pagination={false}
className="table-no-scrollbar"
expandedRowRender={(record) => {
if (
record.visitorInfoVOList &&
record.visitorInfoVOList.length > 0
) {
return (
<Table
columns={expandedColumns}
dataSource={record.visitorInfoVOList}
size={"small"}
className="no-scrollbar expanded-table"
pagination={false}
></Table>
);
} else {
return <div className="live-table--empty">暂无上课数据</div>;
}
}}
></Table>
<div className="student-wrapper">
<section className="class-record-title">学员上课数据</section>
<section>
<Checkbox
style={{lineHeight: '33px'}}
onChange={(e) => {
const param = _.clone(this.state.query);
param.current = 1;
param.liveSignState = e.target.checked ? 'SIGN' : '';
this.setState({
query: param
}, () => {
this.fetchClassList();
})
}}>只看“到课”学员</Checkbox>
<Search
className="student-wrapper__search"
placeholder="搜索学员姓名/手机号"
style={{ width: 200, marginBottom: 0 }}
onSearch={(value) => {
const param = _.clone(this.state.query);
param.nameOrPhone = value;
param.current = 1;
this.setState(
{
query: param,
},
() => {
this.fetchClassList();
}
);
}}
/>
{
hasExportPermission(type) &&
<Button onClick={_.debounce(() => {
if (!classList.length) {
message.warning('暂无数据可导出');
return;
}
if (window.NewVersion) {
this.handleExportV5();
} else {
this.handleExport();
}
}, 500, true)}>导出</Button>
}
</section>
</div>
<div>
<Table
size="small"
columns={this.getColumns("student")}
dataSource={classList}
pagination={false}
className="table-no-scrollbar"
onChange={this.handleTableChange}
expandedRowRender={(record) => {
if (
record.visitorInfoVOList &&
record.visitorInfoVOList.length > 0
) {
return (
<Table
columns={expandedColumns}
dataSource={record.visitorInfoVOList}
size={"small"}
className="no-scrollbar expanded-table"
pagination={false}
></Table>
);
} else {
return <div className="live-table--empty">暂无上课数据</div>;
}
}}
></Table>
<PageControl
size="small"
current={query.current - 1}
pageSize={query.size}
total={total}
toPage={(page) => {
this.fetchClassList(page + 1);
}}
/>
</div>
</div>
</Modal>
);
}
}
export default ClassRecordModal;
.play-back-modal {
.table-no-scrollbar {
margin-top: 16px;
}
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
}
.class-record-modal {
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
.student-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 13px;
margin-top: 25px;
&__search {
margin-right: 12px;
}
}
.class-record-title {
font-size: 16px;
font-weight: 500;
color: rgba(51, 51, 51, 1);
line-height: 22px;
}
.expanded-table {
* {
border: none;
}
}
.live-table--empty {
padding: 20px;
text-align: center;
margin-left: -50px;
color: #999999;
}
.iconfont {
cursor: pointer;
color: #bfbfbf;
}
.bulge {
color: #ff7519;
}
}
import React from 'react';
import { Modal } from 'antd';
import './LackConsumeStudentModal.less';
class LackConsumeStudentModal extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
render() {
const {onOk, onClose, consumeHourNum, calendarTime, lackConsumeStudentList} = this.props;
return (
<Modal
title="学员剩余课时数不足"
visible={true}
okText="继续保存"
onOk={onOk}
onCancel={onClose}
className="lack-consume-student-modal"
>
<p className="desc">每位扣课时学员预计消耗<span className="sign">{(consumeHourNum * calendarTime.length).toFixed(1)}</span>课时(此次排课共计<span className="sign">{calendarTime.length}</span>节课,每节课消耗<span className="sign">{consumeHourNum}</span>课时)</p>
<div className="list-wrap">
<p>以下学员的剩余课时数不足,请提醒学员及时续费</p>
<div className="list">
{lackConsumeStudentList.map(item => {
return <p>{item.name} {item.phone} 剩余{item.consumeHourNum}课时</p>
})}
</div>
</div>
</Modal>
)
}
}
export default LackConsumeStudentModal;
.lack-consume-student-modal {
.desc {
margin-bottom: 12px;
line-height: 22px;
color: #333;
.sign {
color: #ff7519;
}
}
.list-wrap {
padding: 8px;
line-height: 22px;
background: #F3F6FA;
color: #666;
.list {
margin-top: 8px;
max-height: 108px;
overflow: auto;
p {
color: #333;
}
}
}
}
/*
* @Author: Michael
* @Date: 2020-01-29 11:27:34
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-22 16:25:30
* 查看学员名单
*/
import React from "react";
import {
Modal,
Button,
Input,
Table,
Popconfirm,
message,
Tooltip,
} from "antd";
import PropTypes from "prop-types";
import { PageControl } from "@/components";
import SelectStudent from "./select-student/index";
import "./LiveStudentListModal.less";
const { Search } = Input;
const isTeacher = !!window.currentUserInstInfo.teacherId;
const liveTypeMap = {
LIVE: "直播",
PLAYBACK: "回放",
};
const expandedColumns = [
{
title: "类型",
dataIndex: "liveType",
key: "liveType",
render: (text) => <span>{liveTypeMap[text]}</span>,
},
{
title: "进入时间",
dataIndex: "entryTime",
key: "entryTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{
title: "离开时间",
dataIndex: "leaveTime",
key: "leaveTime",
render: (text) => (
<span>{formatDate("YYYY-MM-DD H:i", parseInt(text))}</span>
),
},
{ title: "观看时长", dataIndex: "lookingTime", key: "lookingTime" },
];
const STATUS_ENUM = {
'NORMAL': '在读',
'POTENTIAL': '潜在',
'HISTORY': '历史',
'ABANDON': '废弃',
};
class LiveStudentListModal extends React.Component {
constructor(props) {
super(props);
this.state = {
query: {
current: 1,
size: 10,
nameOrPhone: null,
liveCourseId: props.liveItem.liveCourseId,
},
total: 1,
studentList: [],
studentModal: null,
after: true,
};
}
componentDidMount() {
this.fetchStudentList();
}
fetchStudentList = (current = 1) => {
const query = _.clone(this.state.query);
query.current = current;
window.axios
.Apollo("public/businessLive/getStudentList", query)
.then((res) => {
const { records = [], total } = res.result;
this.setState({
studentList: records,
total,
query,
});
});
};
hanldSelect = () => {
const { query: { liveCourseId } } = this.state;
axios.Apollo('public/businessLive/getCourseDetail', { liveCourseId})
.then((res) => {
const { result = {} } = res;
const { consumeStudentIds, studentIds } = result;
const studentList = [];
const excludeStudentIds = studentIds;
const excludeConsumeStudentIds = _.pluck(consumeStudentIds, 'studentId');
this.setState({ excludeStudentIds, excludeConsumeStudentIds});
_.each(studentIds, (item) => {
studentList.push({ studentId: item });
});
const studentModal = (
<SelectStudent
liveCourseId={liveCourseId}
studentList={studentList}
excludeStudentIds={excludeStudentIds}
after={true}
close={() => { this.setState({ studentModal: null }); }}
onSelect={(studentIds) => {
this.handleSelectStudent(studentIds)
}}
/>
)
this.setState({ studentModal });
})
};
handleSelectStudent = (studentIds) => {
const {
liveType,
liveCourseId,
podium,
quota,
} = this.props.liveItem;
if (liveType !== "SMALL_CLASS_INTERACTION" && (studentIds.length) > 1000) {
message.info(`最多选择1000人`);
return;
} else if (liveType == "SMALL_CLASS_INTERACTION" && (studentIds.length) > podium) {
message.info(`最多选择${podium}人`);
return;
} else {
const param = {
liveCourseId: liveCourseId,
studentIds: studentIds
};
axios.Apollo("public/businessLive/addCourseStu", param).then(res => {
if (res.success) {
this.setState({
studentModal: null
});
message.success("学员变更成功");
this.fetchStudentList();
this.props.refresh();
}
});
}
};
// 移除学员
removeStudent = (studentId) => {
const { liveCourseId } = this.props.liveItem;
const param = {
liveCourseId,
studentId,
};
window.axios
.Apollo("public/businessLive/moveCourseStu", param)
.then((res) => {
message.success("移除学员成功");
this.fetchStudentList(1);
this.props.refresh();
});
};
parseColumns = () => {
const { type, liveItem } = this.props;
const columns = [
{ title: "姓名", dataIndex: "studentName", key: "studentName" },
{
title: "手机号",
dataIndex: "phone",
width: 150,
key: "phone",
render: (text, record) => {
return (
<p>
{!(
(!window.NewVersion && !window.currentUserInstInfo.teacherId) ||
(window.NewVersion && Permission.hasEduStudentPhone())
)
? (text || "").replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3")
: text}
<Tooltip
title={`${record.wechatStatus ? "已绑定微信" : "未绑定微信"}`}
>
<span
className="icon iconfont"
style={
record.wechatStatus
? {
color: "#00D20D",
fontSize: "16px",
marginLeft: 6,
}
: {
color: "#BFBFBF",
fontSize: "16px",
marginLeft: 6,
}
}
>
&#xe68d;
</span>
</Tooltip>
</p>
);
},
}
];
// 非互动班课类型增加学员类型
if (type !== 'interactive') {
columns.push({
title: '学员类型',
key: 'statusEnum',
dataIndex: 'statusEnum',
render: (val) => {
return STATUS_ENUM[val];
}
});
}
// 如果是非视频课, 显示操作的条件是课程未开始,且不是T端
// 如果是视频课,那么只要满足不是T端就可以了
if ((liveItem.courseState === "UN_START" || type === 'videoCourse') && !isTeacher) {
// 未开始
columns.push({
title: "操作",
dataIndex: "operate",
key: "operate",
align:'right',
render: (text, record) => {
return (
<Popconfirm
title="你确定要移出这个学员吗?"
onConfirm={() => {
// 如果是非视频课,且直播间类型是自研, 且晚于开课前30分钟, 不允许移出
if (
liveItem.channel == "XIAOMAI" &&
liveItem.startTime - Date.now() < 1800000
) {
Modal.warning({
title: "不可移出",
icon: (
<span className="icon iconfont default-confirm-icon">
&#xe6f4;
</span>
),
content: "晚于开课前30分钟,不能移出学员",
});
} else {
this.removeStudent(record.studentId);
}
}}
>
<span className="live-operate">移出</span>
</Popconfirm>
);
},
});
}
return columns;
}
render() {
const { studentList, query, total } = this.state;
const { current, size } = query;
return (
<Modal
title="查看学员名单"
visible={true}
width={680}
footer={null}
className="live-student-list-modal"
onCancel={this.props.close}
>
{/* 任意状态都可以添加学员 */}
<div className="live-student-list-modal__operate">
{
!isTeacher &&
<Button type="primary" onClick={this.hanldSelect}>
添加上课学员
</Button>
}
<Search
placeholder="搜索学员姓名/手机号"
style={{ width: 200 }}
onSearch={(value) => {
this.setState({
query: {
...this.state.query,
nameOrPhone: value
}
}, () => {
this.fetchStudentList(1);
});
}}
className="search"
/>
</div>
<Table
size="small"
columns={this.parseColumns()}
dataSource={studentList}
pagination={false}
scroll={{ y: 400 }}
className="live-student-table table-no-scrollbar"
/>
<PageControl
size="small"
current={current - 1}
pageSize={size}
total={Number(total)}
toPage={(page) => {
this.fetchStudentList(page + 1);
}}
/>
{this.state.studentModal}
</Modal>
);
}
}
LiveStudentListModal.propTypes = {
liveItem: PropTypes.object,
status: PropTypes.string,
close: PropTypes.func,
refresh: PropTypes.func, // 刷新列表
};
export default LiveStudentListModal;
.live-student-list-modal {
.student-list-tip {
margin: 0 0 12px;
width: 100%;
height: 32px;
line-height: 32px;
background: #fff0e7;
padding: 0 16px;
.icon {
color: #ff8534;
margin-right: 8px;
}
}
&__operate {
margin-bottom: 12px;
position: relative;
height: 28px;
.export-btn {
margin-left: 12px;
}
.search {
position: absolute;
right: 0;
}
}
&--orange {
color: #FF7519;
}
&--normal{
color: #666666;
}
.live-operate {
color: #FF7519;
cursor: pointer;
}
.expanded-table {
* {
border: none;
}
}
.live-table--empty {
padding: 20px;
text-align: center;
margin-left: -50px;
color: #999999;
}
.live-student-table {
.ant-table-hide-scrollbar {
min-width: 0 !important;
}
::-webkit-scrollbar {
width: 0;
}
.ant-table tbody tr:nth-child(even) {
background: transparent !important;
}
.ant-table-bordered .ant-table-body {
border: none !important;
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-06-22 14:26:37
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-23 09:33:02
*/
import React, { useEffect, useState } from 'react';
import { Modal, Button, Table, Progress, message, Tooltip, Spin, Popconfirm } from 'antd';
import { QuestionCircleOutlined,LoadingOutlined} from "@ant-design/icons";
import _ from 'underscore';
import moment from 'moment';
import User from '@/core/user';
import { suffixType, DEFAULT_SIZE_UNIT, SupportFileType } from '@/common/constants/academic/liveEnum';
import { FileVerifyMap, FileTypeIcon, DISK_MAP } from '@/common/constants/academic/lessonEnum';
import ScanFileModal from '@/modules/cloudClass/prepare-lesson/modal/ScanFileModal'
import SelectPrepareFileModal from '@/modules/cloudClass/prepare-lesson/modal/SelectPrepareFileModal';
import './ManageCoursewareModal.less';
const { instId, teacherId } = window.currentUserInstInfo;
class ManageCoursewareModal extends React.Component {
constructor(props) {
super(props)
this.state = {
list: [],
uploadObject: {},
failObject: {},
cancelObject: {},
editData: {},
scanFileModal: false,
isLessonPermission: false,
diskList: [], // 机构可见的磁盘目录
selectedFileList: []
}
}
componentDidMount() {
this.getCoursewareList();
this.handleFetchDiskList();
if (teacherId) {
this.judgeLessonPermisson();
}
}
// 判断资料云盘权限
judgeLessonPermisson = () => {
const query = {
instId,
permissionCode: "2001"
};
axios.Apollo("public/apollo/judgeLessonPermission", query).then(res => {
this.setState({ isLessonPermission: res.result })
});
};
// 获取课件列表
getCoursewareList(id) {
const { liveCourseId } = this.props.data;
axios.Apollo('anon/businessLive/getCourseDocList', { liveCourseId }).then((res) => {
let newList = [];
const { list } = this.state;
const data = _.find(res.result, item => item.docId == id);
if (id && !_.isEmpty(data)) {
newList = list.map((item) => {
if (item.id == id) {
data.id = data.docId;
if (data.fileSize > 0) {
if (data.fileSize > 0.1 * DEFAULT_SIZE_UNIT) {
data.fileSize = `${(data.fileSize / DEFAULT_SIZE_UNIT).toFixed(1)}M`;
} else {
data.fileSize = `${(data.fileSize / 1000).toFixed(1)}KB`;
}
} else {
data.fileSize = '-';
data.failState = true;
}
return data;
} else {
return item;
}
})
} else {
newList = res.result.map((item) => {
item.id = item.docId;
if (item.fileSize > 0) {
if (item.fileSize > 0.1 * DEFAULT_SIZE_UNIT) {
item.fileSize = `${(item.fileSize / DEFAULT_SIZE_UNIT).toFixed(1)}M`;
} else {
item.fileSize = `${(item.fileSize / 1000).toFixed(1)}KB`;
}
} else {
item.fileSize = '-';
item.failState = true;
}
return item;
});
}
this.setState({ list: newList });
})
}
// 获取机构可见的磁盘目录
handleFetchDiskList = () => {
axios.Apollo('public/apollo/getUserDisk', {}).then((res) => {
const { result = [] } = res;
const diskList = result.map((item) => {
return {
...item,
folderName: DISK_MAP[item.disk]
}
});
this.setState({
diskList,
});
});
}
// 上传文件
addFile() {
// 校验是否欠费
this.handleCheckBalance().then((res) => {
if (!res) return;
// 判断是否早于开课前45分钟
const { startTime } = this.props.data;
const currentTime = new Date().getTime();
if (currentTime >= startTime - 45 * 60 * 1000) {
Modal.info({
title: "不能再上传课件了",
icon: (
<span
className="icon iconfont default-confirm-icon"
style={{ color: "#FFBB54 !important" }}
>
&#xe6f1;
</span>
),
content: "请在开课前45分钟前上传课件,开课后可在客户端中进行上传。",
okText: '我知道了'
});
return;
}
const { list } = this.state;
if (list.length >= 20) {
message.warning('最多上传20个课件');
return null;
}
this.setState({
selectedFileList: list,
showSelectFileModal: true, // 选择文件弹窗
})
})
}
handleAddFile = (addFolderIds) => {
this.setState({
showSelectFileModal: false
});
const { liveCourseId } = this.props.data;
const { teacherId } = window.currentUserInstInfo;
const params = {
addFolderIds,
liveCourseId,
checkTime: true,
operatorId: teacherId || User.tid() || User.aid(),
operatorType: (!teacherId && !User.tid()) ? 1 : 2 // 1: 教务 2: 老师
};
axios.Apollo('public/businessLive/relationLessonFile', params).then((res) => {
this.getCoursewareList();
});
}
// 删除文件
deleteFile(item) {
const { list } = this.state;
window.axios.Apollo('public/businessLive/delCourseDoc', { docId: item.id }).then(() => {
item.docId && message.success('删除成功')
})
const _list = _.reject(list, (data) => data.id == item.id);
this.setState({ list: _list });
}
// 预览文件
handleScanFile(item) {
if (!item.srcDocUrl) return null;
const suffix = _.last(item.fileName.split('.')).toLowerCase();
const type = suffixType[suffix]
const fileType = FileVerifyMap[type].type;
switch (fileType) {
case "PDF":
window.open(item.srcDocUrl, "_blank");
break;
case "Excel":
case "EXCEL":
case "PPT":
case "PPTX":
case "word":
case "WORD":
case "DOCX":
case "DOC":
let size = parseFloat(item.fileSize.replace(/M$|KB$/g, ''));
if (item.fileSize.includes('KB')) {
size = 0;
}
if (((fileType == 'word' || fileType == 'PPT') && size > 10) || ((fileType == 'Excel') && size > 5)) {
Modal.confirm({
title: '抱歉,不能在线预览',
content: '由于文件较大,不支持在线预览,请下载后再查看',
icon: <QuestionCircleOutlined />,
okText:"下载",
onOk:() => {
const a = document.createElement('a');
a.href = item.srcDocUrl;
a.click();
}
});
} else {
const scanUrl = "https://view.officeapps.live.com/op/view.aspx?src=" + encodeURIComponent(item.srcDocUrl);
window.open(scanUrl, "_blank");
}
break;
case "JPG":
case "PNG":
case "MP4":
case "MP3":
this.setState({ scanFileModal: true, editData: { fileType, ossAddress: item.srcDocUrl } })
break;
default:
break;
}
};
// 校验余额
handleCheckBalance = async () => {
const { type } = this.props;
const balanceRes = await axios.Business("public/liveAssets/query", { instId });
// 判断是否欠费,旗舰版用户不需要校验余额
const ultimateRes = await axios.Business('public/inst/checkInstProduct', {
instId,
productCodeList: ['ULTIMATESELL', 'PIP_TO_ULTIMATE', 'HIGH_TO_ULTIMATE']
});
const { result } = balanceRes;
// balance小于0表示已经欠费
if ((!result || result.balance <= 0) && !ultimateRes.result && type === 'interactive') {
Modal.info({
title: '无法继续操作',
content: '直播服务已升级,请联系运营老师。',
icon: <span className="icon iconfont default-confirm-icon">&#xe6f4;</span>
})
return false;
}
return true;
};
render() {
const columns = [
{
title: "名称",
width: "25%",
dataIndex: "name",
render: (_value, item) => {
const suffix = _.last(item.fileName.split('.')).toLowerCase();
const fileType = suffixType[suffix]
const antIcon = <LoadingOutlined/>;
const type = FileVerifyMap[fileType].type;
return <div className="courseware-name" onClick={() => this.handleScanFile(item)}>
{(type === 'JPG' || type === 'PNG') && item.progress ?
<Spin indicator={antIcon} />
:<img
src={FileTypeIcon[FileVerifyMap[fileType].type] || (item.docUrls[0] || {}).conversionFileUrl}
alt=""
className="item-img"
/>
}
<Tooltip title={item.fileName}><span className="name">{item.fileName}</span></Tooltip>
</div>
},
},
{
title: "创建人",
width: "12%",
dataIndex: "adminName",
render: (_value, item) => {
return <span>{item.operatorName}</span>
},
},
{
title: "上传时间",
width: "20%",
dataIndex: "created",
render: (_value, item) => {
return item.failState ? '-' : <span>{moment(item.created).format('YYYY-MM-DD HH:mm')}</span>
},
},
{
title: "大小",
width: "12%",
dataIndex: "size",
render: (_value, item) => {
return <span>{item.fileSize}</span>
},
},
{
title: "操作",
width: "16%",
dataIndex: "control",
render: (_value, item) => {
const { uploadObject, failObject, cancelObject } = this.state;
const uploadFail = failObject[item.id];
// 上课前45分钟/上课中/已结束的情况下都不可操作
if (this.props.data.startTime < Date.now() + 2700000 || item.progress || uploadFail) {
return <span>-</span>
}
return (
<Popconfirm
title="你确定要删除这个课件吗?"
onConfirm={() => this.deleteFile(item)}
onCancel={() => { }}
>
<span style={{
color: '#FF7519',
cursor: 'pointer'
}}>删除</span>
</Popconfirm>
)
},
},
]
const {
list, scanFileModal, editData, cancelObject,
showSelectFileModal, selectedFileList,
diskList, currentRootDisk, isLessonPermission
} = this.state;
const _list = _.reject(list, (item) => cancelObject[item.id]);
return (
<Modal
visible={true}
title="课件管理"
footer={null}
className="manage-courseware-modal"
width={_.isEmpty(_list) ? 680 : 800}
onCancel={() => {
this.props.onCancel()
}}
>
{_.isEmpty(_list) ?
<div className="empty-body">
<img className="empty-image" src="https://image.xiaomaiketang.com/xm/s8xkAPCDex.png" alt="" />
{
((!teacherId && Permission.hasClassBook()) || isLessonPermission) &&
<Button
className="empty-button"
type="primary"
onClick={() => this.addFile()}
>上传课件</Button>
}
<p className="empty-tip">提前上传直播需要的课件和素材,直播将会变得更便捷!</p>
</div>
: <div className="manage-body">
<div className="header">
{
((!teacherId && Permission.hasClassBook()) || isLessonPermission) &&
<Button
className="header-button"
type="primary"
onClick={() => this.addFile()}
>上传课件</Button>
}
</div>
<Table
size="small"
pagination={false}
rowKey={record => record.id}
dataSource={_list}
columns={columns}
bordered
/>
</div>
}
{
scanFileModal &&
<ScanFileModal
item={editData}
fileType={editData.fileType}
close={() => this.setState({ scanFileModal: false })}
/>
}
<SelectPrepareFileModal
multiple={true}
scene="liveCourse"
operateType="select"
isOpen={showSelectFileModal}
diskList={diskList}
selectedFileList={selectedFileList}
onClose={() => {
this.setState({ showSelectFileModal: false })
}}
onSelect={this.handleAddFile}
/>
</Modal>
)
}
}
export default ManageCoursewareModal;
\ No newline at end of file
.manage-courseware-modal {
.empty-body {
padding: 16px 0;
.empty-tip {
text-align: center;
color: #333;
}
.empty-image {
display: block;
margin: 24px auto 12px;
}
.empty-button {
display: block;
margin: 0 auto 8px;
}
.empty-text {
color: #999;
text-align: center;
}
}
.manage-body {
.header {
margin-bottom: 16px;
display: flex;
align-items: center;
.header-button {
margin-right: 8px;
}
.header-tip {
color: #666;
}
}
.ant-table-bordered .ant-table-thead tr th:last-child {
border-right: none !important;
}
.ant-table-bordered .ant-table-tbody tr td:last-child {
border-right: none !important;
}
td {
color: #666;
}
.courseware-name {
display: flex;
align-items: center;
cursor: pointer;
.item-img {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 2px;
margin-right: 8px;
}
.name {
max-width: 154px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
.name {
color: #FF7519;
}
}
}
}
}
\ No newline at end of file
/*
* @Description: 回放记录
* @Author: zhangyi
* @Date: 2020-05-12 09:43:48
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 16:23:05
*/
import React, { useState, useEffect } from "react";
import { Modal, Table, Button, message } from "antd";
import Bus from '@/core/bus';
import { PageControl } from "@/components";
import hasExportPermission from '../utils/hasExportPermission';
import dealTimeDuration from '../utils/dealTimeDuration';
import "./ClassRecordModal.less";
const liveTypeMap = {
USER: "学生",
ANCHOR: "老师",
ADMIN: "助教",
};
class PlayBackRecordModal extends React.Component {
constructor(props) {
super(props);
this.state = {
playBackList: [],
query: {
current: 1,
size: 10,
liveCourseId: props.liveItem.liveCourseId,
},
total: 0,
recordDuration: null,
totalWatchNum: 0,
};
}
componentDidMount() {
this.fetchPlayBackList();
}
fetchPlayBackList = (page = 1) => {
const params = _.clone(this.state.query);
params.current = page;
window.axios
.Apollo("public/businessLive/queryUserReplayRecordPage", params)
.then((res) => {
const { records = [], total } = res.result;
this.setState({
query: params,
total,
playBackList: records,
});
});
};
fetchAllStatistics = () => {
const { liveCourseId } = this.props.liveItem;
window.axios
.Apollo("public/businessLive/queryReplayStatistics", {
liveCourseId,
})
.then((res) => {
if (res.result) {
const { recordDuration = 0, totalWatchNum = 0 } = res.result;
this.setState({
recordDuration,
totalWatchNum,
});
}
});
};
getLastTime = (time = 0) => {
const diff = Math.floor(time % 3600);
const hours = Math.floor(time / 3600);
const mins = Math.floor(diff / 60);
const seconds = Math.floor(time % 60);
return hours + "小时" + mins + "分";
};
// 导出
handleExport = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'api-b/b/lesson/exportLargeClassLiveAsync' : 'api-b/b/lesson/exportClassInteractionLiveSync';
window.axios.post(url, {
liveCourseId,
exportLiveType: 0
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“导出中心”进行下载',
okText: '我知道了',
});
});
}
handleExportV5 = () => {
const { liveItem, type } = this.props;
const { liveCourseId } = liveItem;
const url = !type ? 'public/businessLive/exportLargeClassLiveAsync' : 'public/businessLive/exportClassInteractionLiveSync';
window.axios.Apollo(url, {
liveCourseId,
exportLiveType: 'PLAY_BACK'
}).then((res) => {
Bus.trigger('get_download_count');
Modal.success({
title: '导出任务提交成功',
content: '请前往右上角的“任务中心”进行下载',
okText: '我知道了',
});
});
}
render() {
const columns = [
{
title: "观看者姓名",
dataIndex: "userName",
},
{
title: "观看者手机号",
dataIndex: "phone",
render: (text, record) => {
return (
<p>
{!(
(!window.NewVersion && !window.currentUserInstInfo.teacherId) ||
(window.NewVersion && Permission.hasEduStudentPhone())
)
? (text || "").replace(/(\d{3})(\d{4})(\d{4})/, "$1****$3")
: text}
</p>
);
},
},
{
title: "观看者类型",
dataIndex: "liveRole",
key: "liveRole",
render: (text) => <span>{liveTypeMap[text]}</span>,
},
{
title: "开始观看时间",
dataIndex: "entryTime",
key: "entryTime",
render: (text) => (
<span>{text ? formatDate("YYYY-MM-DD H:i", parseInt(text)) : '-'}</span>
),
},
{
title: "观看时长",
dataIndex: "lookingDuration",
key: "lookingDuration",
render: (text) => {
return <span>{text ? dealTimeDuration(text) : '-'}</span>;
},
},
];
const {
query,
total,
playBackList,
totalWatchNum,
recordDuration,
} = this.state;
const { type } = this.props;
return (
<Modal
title="回放记录"
className="play-back-modal"
width={680}
visible={true}
footer={null}
onCancel={() => {
this.props.close();
}}
>
{
hasExportPermission(type) &&
<Button onClick={_.debounce(() => {
if (!playBackList.length) {
message.warning('暂无数据可导出');
return;
}
if (window.NewVersion) {
this.handleExportV5();
} else {
this.handleExport();
}
}, 500, true)}>导出</Button>
}
<Table
size="small"
columns={columns}
dataSource={playBackList}
pagination={false}
className="table-no-scrollbar"
/>
<PageControl
size="small"
current={query.current - 1}
pageSize={query.size}
total={total}
toPage={(page) => {
this.fetchPlayBackList(page + 1);
}}
/>
</Modal>
);
}
}
export default PlayBackRecordModal;
/*
* @Author: 吴文洁
* @Date: 2020-07-23 14:54:16
* @LastEditors: 吴文洁
* @LastEditTime: 2020-08-28 10:49:49
* @Description: 大班直播课预览弹窗
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Modal } from 'antd';
import './PreviewCourseModal.less';
class PreviewCourseModal extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
dealWithTime = (startTime, endTime) => {
const startDate = new Date(Number(startTime));
const endDate = new Date(Number(endTime));
const year = startDate.getFullYear();
const month = (startDate.getMonth() + 1) < 10 ? `0${startDate.getMonth() + 1}` : startDate.getMonth() + 1;
const day = startDate.getDate() < 10 ? `0${startDate.getDate()}` : startDate.getDate();
const startHour = startDate.getHours() < 10 ? `0${startDate.getHours()}` : startDate.getHours();
const startMinute = startDate.getMinutes() < 10 ? `0${startDate.getMinutes()}` : startDate.getMinutes();
const endHour = endDate.getHours() < 10 ? `0${endDate.getHours()}` : endDate.getHours();
const endMinute = endDate.getMinutes() < 10 ? `0${endDate.getMinutes()}` : endDate.getMinutes();
const liveDateStr = `${year}-${month}-${day}`;
const startTimeStr = `${startHour}:${startMinute}`;
const endTimeStr = `${endHour}:${endMinute}`;
return {
liveDateStr,
startTimeStr,
endTimeStr,
};
}
render() {
const { courseBasinInfo, courseClassInfo = {}, courseIntroInfo, type } = this.props;
const { coverUrl, courseName, scheduleVideoUrl } = courseBasinInfo;
const { liveDate, timeHorizonStart, timeHorizonEnd, nickname } = courseClassInfo;
const { liveCourseMediaRequests } = courseIntroInfo;
let liveDateStr, startTimeStr, endTimeStr;
if (liveDate) {
const _liveDate = moment(liveDate).format("YYYY-MM-DD");
const _timeHorizonStart = moment(timeHorizonStart).format('HH:mm');
const _timeHorizonEnd = moment(timeHorizonEnd).format('HH:mm');
const startTime = moment(_liveDate + ' ' + _timeHorizonStart).format('x');
const endTime = moment(_liveDate + ' ' + _timeHorizonEnd).format('x');
const {
liveDateStr: _liveDateStr,
startTimeStr: _startTimeStr,
endTimeStr: _endTimeStr
} = this.dealWithTime(startTime, endTime);
liveDateStr = _liveDateStr;
startTimeStr = _startTimeStr,
endTimeStr = _endTimeStr;
}
return (
<Modal
title="预览"
visible={true}
width={680}
onCancel={this.props.close}
footer={null}
className="preview-live-course-modal"
>
<div className="container__wrap">
<div className="container">
<div className="container__header">
{
type === 'videoCourse' ?
<video
controls
src={scheduleVideoUrl}
poster={coverUrl ? coverUrl : `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast`}
className="course-url"
/> :
<img src={coverUrl} className="course-cover" />
}
</div>
{
type === 'videoCourse' ?
<div className="container__body">
<div className="title__name">{courseName}</div>
<div className="title__inst-name">{window.currentUserInstInfo.name}</div>
</div> :
<div className="container__body">
<div className="container__body__title">
<div className="title__name">{courseName}</div>
<div className="title__state">待开课</div>
</div>
<div className="container__body__time">
<span className="time__label">上课时间:</span>
<span className="time__value">
{
liveDate && timeHorizonStart && timeHorizonEnd &&
[
<span>{liveDateStr}&nbsp;</span>,
<span>{startTimeStr}~{endTimeStr }</span>
]
}
</span>
</div>
<div className="container__body__teacher">
<span className="teacher__label">上课老师:</span>
<span className="teacher__value">{nickname}</span>
</div>
</div>
}
<div className="container__introduction">
<div className="container__introduction__title">直播简介</div>
<div className="container__introduction__list editor-box">
{
liveCourseMediaRequests.map((item, index) => {
if (item.mediaType === 'TEXT') {
return (
<div
className="intro-item text"
dangerouslySetInnerHTML={{
__html: item.mediaContent
}}
/>
)
}
if (item.mediaType === 'PICTURE') {
return (
<div className="intro-item picture">
<img src={item.mediaUrl} />
</div>
)
}
})
}
</div>
</div>
</div>
</div>
</Modal>
)
}
}
export default PreviewCourseModal;
.preview-live-course-modal {
.ant-modal-body {
background-image: url('https://image.xiaomaiketang.com/xm/xZWdziTCAf.png');
background-size: 100% 100%;
}
.container__wrap {
width: 340px;
height: 618px;
padding: 67px 46px 48px 47px;
margin: auto;
background-image: url('https://image.xiaomaiketang.com/xm/DHMzHiGc2E.png');
background-size: 100% 100%;
}
.container {
overflow: scroll;
height: 100%;;
.course-cover, .course-url {
width: 100%;
height: 141px;
background: #000;
}
&__body {
background-color: #FFF;
padding: 7px 0 11px 0;;
.title__name {
color: #000;
}
.title__inst-name {
color: #666;
font-size: 12px;
margin-top: 4px;
}
&__title {
display: flex;
justify-content: space-between;
.title__name {
line-height: 20px;
color: #000;
font-weight: 500;
}
.title__state {
min-width: 40px;
height: 17px;
padding: 0 2px;
margin-left: 6px;
font-size: 12px;
color: #FFF;
background-color: #34B88B;
}
}
&__time, &__teacher {
display: flex;
align-items: center;
margin-top: 8px;
color: #999;
line-height: 17px;
span {
font-size: 12px;
}
}
&__teacher {
margin-top: 4px;
}
}
&__introduction {
margin-top: 10px;
padding: 12px 0;
position: relative;
&::before {
content: '';
position: absolute;
top: -12px;
left: 0;
width: 100%;
height: 10px;
background-color: #F4F6FA;
}
&__title {
font-size: 12px;
color: #333;
text-align: center;
line-height: 17px;
position: relative;
&::before,
&::after {
position: absolute;
content: "";
top: 8px;
width: 13px;
height: 1px;
background-color: #ccc;
}
&::before {
left: 32%;
}
&::after {
right: 32%;
}
}
&__list {
margin-top: 12px;
.intro-item:not(:first-child) {
margin-top: 13px;
}
.text {
color: #666;
line-height: 17px;
p {
font-size: 12px;
}
}
.picture {
img {
width: 100%;
}
}
}
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-07-20 19:12:49
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-20 20:25:13
* @Description: 大班直播分享弹窗
*/
import React from 'react';
import { Modal, Input, Button, message } from 'antd';
import domtoimage from 'dom-to-image';
import html2canvas from 'html2canvas';
import qrcode from "@/libs/qrcode/qrcode.js";
import './ShareLiveModal.less';
const BASE_IMG = require('@/images/xiaomai-IMG.png');
const { name, banner = BASE_IMG } = currentUserInstInfo;
const DEFAULT_COVER = 'https://image.xiaomaiketang.com/xm/YNfi45JwFA.png';
class ShareLiveModal extends React.Component {
constructor(props) {
super(props);
this.state = {
shareUrl: 'https://xiaomai5.com/liveShare?courseId=12'
}
}
componentDidMount() {
// 获取短链接
this.handleConvertShortUrl();
}
handleConvertShortUrl = () => {
const { longUrl } = this.props.data;
// 发请求
axios.Sales('public/businessShow/convertShortUrls', {
urls: [longUrl]
}).then((res) => {
const { result = [] } = res;
this.setState({
shareUrl: result[0].shortUrl
}, () => {
const qrcodeWrapDom = document.querySelector('#qrcodeWrap');
const qrcodeNode = new qrcode({
text: this.state.shareUrl,
size: 98,
})
qrcodeWrapDom.appendChild(qrcodeNode);
});
})
}
componentWillUnmount() {
// 页面销毁之前清空定时器
clearTimeout(this.timer);
}
// 下载海报
handleDownloadPoster = () => {
const dom = document.querySelector('#poster');
html2canvas(dom, {
useCORS: true,
}).then(canvas => {
const download = document.createElement('a');
const { courseName } = this.props.data;
const dataUrl = canvas.toDataURL('image/png');
$(download).attr('href', dataUrl).attr('download', `${courseName}.png`).get(0).click();
})
}
// 复制分享链接
handleCopy = () => {
const textContent = document.getElementById('shareUrl').innerText;
window.copyText(textContent);
message.success('复制成功!');
}
render() {
const { needStr, data, type } = this.props;
const { courseName, coverUrl = DEFAULT_COVER, scheduleVideoUrl } = data;
const { shareUrl } = this.state;
// 判断是否是默认图, 默认图不需要在URL后面增加字符串
const isDefaultCover = coverUrl === DEFAULT_COVER;
const coverImgSrc = type === 'videoClass'
// 如果是默认图, 显示视频的第一帧, 否则显示上传的视频封面
? ((!coverUrl || isDefaultCover)
? `${scheduleVideoUrl}?x-oss-process=video/snapshot,t_0,m_fast&anystring=anystring`
: `${coverUrl}${!needStr ? '&anystring=anystring': ''}`)
: `${coverUrl}${(!needStr && !isDefaultCover) ? '&anystring=anystring' : ''}`
return (
<Modal
title={type === 'videoClass' ? '分享视频课' : '分享直播课'}
width={680}
visible={true}
footer={null}
className="share-live-modal"
onCancel={this.props.close}
>
<div className="left" id="poster">
<div className="course-name">{`【${courseName}】开课啦,快来学习!`}</div>
<img
src={coverImgSrc}
crossOrigin="*"
className="course-cover"
/>
<div className="qrcode-wrap">
<div className="qrcode-wrap__left">
<div className="text">长按识别二维码进入观看</div>
<img className="finger" src="https://image.xiaomaiketang.com/xm/thpkWDwJsC.png"/>
</div>
<div className="qrcode-wrap__right" id="qrcodeWrap">
</div>
</div>
<div className="inst-name">
<span className="icon iconfont">&#xe7b1;</span>
<span className="text">{name}</span>
</div>
</div>
<div className="right">
<div className="share-url right__item">
<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 className="share-poster right__item">
<div className="title">② 海报分享</div>
<div className="sub-title">学生可通过微信识别二维码,报名观看直播</div>
<div className="content" onClick={this.handleDownloadPoster}>下载海报</div>
</div>
</div>
</Modal>
)
}
}
export default ShareLiveModal;
.share-live-modal {
.ant-modal-body {
display: flex;
.left {
width: 303px;
padding: 20px;
margin: 0 32px 0 16px;
box-shadow:0px 2px 10px 0px rgba(0,0,0,0.05);
border-radius: 12px;
.course-name {
color: #333;
font-size: 16px;
font-weight: 500;
line-height: 20px;
}
.course-cover {
width: 263px;
height: 143px;
border-radius: 6px;
margin-top: 8px;
}
.qrcode-wrap {
padding: 0 16px;
display: flex;
align-items: center;
margin: 24px 0 16px 0;
&__left {
width: 98px;
text-align: center;
margin-right: 22px;
.text {
line-height: 20px;
}
.finger {
width: 40px;
height: 40px;
margin-top: 8px;
}
}
&__right {
width: 110px;
height: 110px;
padding: 6px
}
}
.inst-name {
background-color: #FAFAFA;
min-height: 36px;
border-radius: 9px;
padding: 8px 16px;
display: flex;
align-items: center;
.iconfont {
color: #999;
margin-right: 4px;
}
.text {
font-size: 12px;
color: #999;
}
}
}
.right {
.title {
color: #333;
font-weight: 500;
}
.sub-title {
color: #999;
margin-top: 16px;
}
.content {
display: flex;
align-items: center;
margin-top: 8px;
.share-url {
width: 212px;
overflow: hidden;
height: 28px;
line-height: 28px;
border-radius: 4px 0 0 4px;
padding-left: 12px;
white-space: nowrap;
color: #999999;
background: #EFEFEF;
}
.ant-btn {
margin-left: -2px;
}
}
.share-poster {
margin-top: 40px;
.content {
color: #FF7519;
cursor: pointer;
}
}
}
}
}
\ No newline at end of file
import React from 'react';
import { Modal, Button } from "antd";
import "./StartLiveModal.less"
class StartLiveModal extends React.Component {
constructor(props) {
super(props);
this.state = {
download: "",
downloadMac: "",
protocol: "",
protocolMac: ""
};
}
componentDidMount() {
this.fetchLaunchInfo();
}
fetchLaunchInfo = () => {
const param = {
liveCourseId: this.props.liveCourseId
};
window.axios.Apollo('public/businessLive/courseLaunch', param).then(res => {
const { result } = res;
this.setState({
download: result.download,
downloadMac: result.downloadMac,
protocol: result.protocol,
protocolMac: result.protocolMac,
});
})
}
downloadAPP = () => {
const { download, downloadMac } = this.state;
if (!download || !downloadMac) {
return;
}
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const aTag = document.createElement('a');
document.body.appendChild(aTag);
if (isMac) {
aTag.href = encodeURI(downloadMac);
} else {
aTag.href = encodeURI(download);
}
aTag.target = '_blank';
aTag.click();
document.body.removeChild(aTag);
this.props.close();
}
launchApp = () => {
const { protocol, protocolMac } = this.state;
if (!protocol || !protocolMac) {
return;
}
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
const aTag = document.createElement('a');
document.body.appendChild(aTag);
if (isMac) {
aTag.href = encodeURI(protocolMac);
} else {
aTag.href = encodeURI(protocol);
}
aTag.target = '_blank';
aTag.click();
document.body.removeChild(aTag);
}
render() {
return (
<Modal
title="开始直播"
visible={true}
width={550}
footer={null}
onCancel={this.props.close}
>
<div className="live-start-modal">
<img src="https://image.xiaomaiketang.com/xm/K2sJJHG3pa.png" alt="" className="live-img"></img>
<div className="live-text">
<p className="live-title">没有安装直播客户端</p>
<p className="live-instruction">首次上课需要下载云直播客户端,请先下载并安装。</p>
</div>
<Button type="primary" className="live-btn" onClick={this.downloadAPP}>立即下载</Button>
</div>
<div className="live-start-modal">
<img src="https://image.xiaomaiketang.com/xm/wPwRdaa7MM.png" alt="" className="live-img"></img>
<div className="live-text">
<p className="live-title">已安装直播客户端</p>
<p className="live-instruction">已安装云直播客户端,请直接启动上课</p>
</div>
<Button type="primary" className="live-btn" onClick={this.launchApp}>开始直播</Button>
</div>
</Modal>
);
}
}
export default StartLiveModal;
.live-start-modal {
position: relative;
display: flex;
align-items: center;
margin-bottom: 26px;
.live-img {
width: 80px;
}
.live-text {
width: 253px;
margin: 12px;
.live-title {
font-size: 16px;
color: rgba(51, 51, 51, 1);
line-height: 22px;
}
.live-instruction {
font-size: 14px;
color: rgba(102, 102, 102, 1);
line-height: 20px;
}
}
.live-btn{
position: absolute;
right: 0;
}
}
/*
* @Author: 吴文洁
* @Date: 2020-07-20 17:21:04
* @Last Modified by: 吴文洁
* @Last Modified time: 2020-07-20 18:19:33
* @Description: 老师直播说明弹窗
*/
import React from 'react';
import { Modal } from 'antd';
import TeacherTip from './TeacherTip';
import './TeacherLiveModal.less';
class TeacherLiveModal extends React.Component {
constructor(props) {
super(props);
this.state = {
isXiaomai: false,
}
}
componentWillMount() {
this.getLivePermission();
}
getLivePermission() {
axios.Apollo("public/businessLive/queryLiveAccount").then((res) => {
const list = res.result;
const isXiaomai = _.some(list, (item) => item.channel === "XIAOMAI");
this.setState({ isXiaomai });
});
}
render() {
const { isXiaomai } = this.state;
return (
<Modal
title="老师直播说明"
className="teacher-live-modal"
visible={true}
width={740}
onCancel={this.props.close}
footer={null}
>
<TeacherTip isXiaomai={isXiaomai} />
</Modal>
)
}
}
export default TeacherLiveModal;
\ No newline at end of file
.teacher-live-modal {
.modal-title {
font-size: 16px;
color: rgba(51, 51, 51, 1);
line-height: 22px;
}
.modal-tip-box {
width: 502px;
height: 196px;
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(232, 232, 232, 1);
padding: 16px;
margin-top: 8px;
.modal-sub-title {
font-size: 14px;
color: rgba(51, 51, 51, 1);
line-height: 20px;
margin-bottom: 8px;
}
.modal-sub-tip {
font-size: 14px;
color: rgba(153, 153, 153, 1);
line-height: 20px;
margin: 8px 0;
}
}
}
\ No newline at end of file
/*
* @Author: zhangyi
* @Date: 2020-02-07 14:35:21
* @Last Modified by: chenshu
* @Last Modified time: 2020-09-10 15:30:29
*/
import React, { useEffect, useState } from 'react';
import './TeacherTip.less';
class TeacherTip extends React.Component {
constructor(props) {
super(props)
this.state = {
}
}
goclick = () => {
if (window.NewVersion) {
window.open("https://b.xiaomai5.com/#/login", "_blank");
} else {
window.open("https://b.xiaomai5.com/teacher.html", "_blank");
}
};
render() {
const { isXiaomai } = this.props;
return (
<div className="teacher__container">
<div className="teacher__container__child teacher">
<div className="teacher__child__title"> <span>上课老师如何直播?</span></div>
{window.NewVersion ?
<div className="teacher__child__box">
<div className="teacher__child__box__item">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/pFbfzS5TEnrMCPTGTG6JifJ67SW7EajfXatWrSWY8HKkTPWs1581060346720" className="teacher__img" />
<p>进入直播课表</p>
<div className="teacher__child__box__item__text">新建直播课、设置上课时间、选择上课学员,选择上课老师</div>
</div>
<div className="teacher__child__box__item ml10">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/JXA6WzYWABrkMtT2SzNPFAQR2tMNfMhD7rr4PQJGmad8w7s81581060291294" className="teacher__img" />
<p>课前准备</p>
<div className="teacher__child__box__item__text">准备PPT等课件、下载直播客户端、发送学员上课通知</div>
</div>
<div className="teacher__child__box__item ml10">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/5CwYmctQdCdeQaMBJHeawhDNfd35jRCQ4MzQNnZDR5WtbbDf1581060318175" className="teacher__img" />
<p>开始上课</p>
<div className="teacher__child__box__item__text">上课老师账号点击“开始上课”,唤起直播客户端进入直播间授课</div>
</div>
</div>
:<div className="teacher__child__box">
<div className="teacher__child__box__item">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/JXA6WzYWABrkMtT2SzNPFAQR2tMNfMhD7rr4PQJGmad8w7s81581060291294" className="teacher__img" />
<p>课前准备</p>
<div className="teacher__child__box__item__text">准备PPT等课件、下载直播客户端、发送学员上课通知</div>
</div>
<div className="teacher__child__box__item ml10">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/pFbfzS5TEnrMCPTGTG6JifJ67SW7EajfXatWrSWY8HKkTPWs1581060346720" className="teacher__img" />
<p>登录电脑端</p>
<div className="teacher__child__box__item__text">直播课表<span style={{color:'#FF7519',fontSize:'14px',cursor:'pointer'}} onClick={() => {this.goclick()}}>{window.NewVersion ? 'https://b.xiaomai5.com/#/login':'https://b.xiaomai5.com/teacher.html'}</span></div>
</div>
<div className="teacher__child__box__item ml10">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/5CwYmctQdCdeQaMBJHeawhDNfd35jRCQ4MzQNnZDR5WtbbDf1581060318175" className="teacher__img" />
<p>进入直播课表</p>
<div className="teacher__child__box__item__text">通过微信家长端“服务”页面,点击“直播课”,进入直播课表</div>
</div>
</div>
}
</div>
<div className="teacher__container__child student">
<div className="teacher__child__title"><span>学员如何观看直播?</span></div>
{isXiaomai ?
<div className="teacher__child__box">
<div className="teacher__child__box__item">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1584348178767.png?x-oss-process=image/resize,w_300,limit_0" className="student__img"/>
<p>进入直播课表</p>
<div className="teacher__child__box__item__text">通过应用市场搜索“每课学堂APP“,进入直播课表</div>
</div>
<div className="teacher__child__box__item ml37">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1584348215146.png?x-oss-process=image/resize,w_300,limit_0" className="student__img" />
<p>找到直播课</p>
<div className="teacher__child__box__item__text">在直播课的课表中,找到本次需要上的直播课</div>
</div>
<div className="teacher__child__box__item ml37">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/1584348242614.png?x-oss-process=image/resize,w_300,limit_0" className="student__img"/>
<p>进入直播间</p>
<div className="teacher__child__box__item__text">在“直播课详情”页中,点击“进入直播间”按钮,进入直播间</div>
</div>
</div>
: <div className="teacher__child__box">
<div className="teacher__child__box__item">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/MzXs4GCmTsQ5SEeB5ZKdQbstrZD3mfcKDtZpFzZS723P48jc1581149424829" className="student__img"/>
<p>进入家长端</p>
<div className="teacher__child__box__item__text">在“家长端-服务”中找到“直播课”的入口</div>
</div>
<div className="teacher__child__box__item ml37">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/DcxB2QhepAJ43xYwQ6tyDrdmyTQwFZzWwmwmeTPbaNEpADY81581060047852" className="student__img" />
<p>找到直播课</p>
<div className="teacher__child__box__item__text">在直播课的课表中,找到本次需要上的直播课</div>
</div>
<div className="teacher__child__box__item ml37">
<img src="https://xiaomai-image.oss-cn-hangzhou.aliyuncs.com/pyzPekG6EsG7Y3JH28KKdJJFb3xKNfXMHWwY8kdzFbjEjmb31581060110133" className="student__img"/>
<p>进入直播间</p>
<div className="teacher__child__box__item__text">在“直播课详情”页中,点击“进入直播间”按钮,进入直播间</div>
</div>
</div>
}
</div>
</div>
)
}
}
export default TeacherTip;
\ No newline at end of file
.teacher__container {
&__child {
width:692px;
border:1px solid rgba(232,232,232,1);
&.teacher {
padding: 16px 13px;
margin-bottom: 16px;
}
&.student{
padding: 16px 26px;
}
.teacher__child__title {
position: relative;
height: 22px;
text-align: center;
span {
display: inline-block;
position: absolute;
top:0;
left: 0;
right:0;
margin: 0 auto;
z-index: 3;
font-weight:400;
font-size: 16px;
color:rgba(51,51,51,1);
line-height:22px;
}
&::after {
content: "";
display: block;
position: absolute;
bottom: 0;
width:150px;
height:6px;
background:rgba(252,156,107,0.7);
border-radius:3px;
left: 0;
right:0;
margin: 0 auto;
z-index: 1;
}
}
.teacher__child__box {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: 6px;
&__item {
text-align: center;
.teacher__img {
width: 215px;
height: 145px;
}
.student__img {
width: 190px;
height: 250px;
}
> p {
font-size:16px;
color:rgba(51,51,51,1);
line-height:22px;
}
&__text {
font-size:14px;
color:rgba(102,102,102,1);
line-height:20px;
}
}
}
}
.ml10 {
margin-left: 10px;
}
.ml37 {
margin-left: 37px;
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-07 16:28:41
* @LastEditors: 吴文洁
* @LastEditTime: 2020-09-23 20:36:51
* @Description: 选择学员-筛选组件
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Row, Col } from 'antd';
import Bus from '@/core/bus';
import { SearchBar } from '@/components';
import ClassSearchSelect from '@/modules/common/ClassSearchSelect';
import CourseSearchSelect from '@/modules/common/CourseSearchSelect';
import StaticSelect from '@/modules/common/StaticSelect';
let resourceData = {};
_.map(window.RESOURCE, item => {
resourceData[item.code] = item.name
});
class FilterContent extends React.Component {
constructor(props) {
super(props);
this.state = {
salesData: {}, // 跟进人列表
}
}
componentDidMount() {
//获取跟进人列表
this.getPotentialAccountList()
}
//获取跟进人列表
getPotentialAccountList() {
const url = 'api-b/b/potential/account/list';
window.axios.post(url).then((res) => {
let salesData = {}
_.map(res.data, item => {
salesData[item.id] = item.name;
});
salesData[0] = '待分配';
this.setState({ salesData });
});
}
render() {
const { salesData } = this.state;
const { query, type, studentType } = this.props;
const {
status, nameOrPhone, classId, className,
courseId, saleId, resourceType
} = query;
// 是否是在读学员
const isNormal = (status === 'NORMAL' || status === 1);
// 是否是潜在学员
const isPotential = (status === 'POTENTIAL' || status === 2);
// 是否是历史学员
const isHistory = (status === 'HISTORY' || status === 4);
return (
<div className="filter-content">
<Row
style={{
display: 'flex',
justifyContent: 'flexStart',
alignItems: 'top',
marginBottom: 12
}}
>
<Col span={7}>
<SearchBar
type='select'
placeholder='请输入学员姓名/手机号'
options={{ 'name': '学员姓名', 'phone': '手机号' }}
value={nameOrPhone}
onSearch={(value) => {
this.props.onChange('nameOrPhone', value);
}}
/>
</Col>
{
// 在读学员显示班级和课程筛选项
((isNormal && studentType === 'DEDUCTION' ) || isHistory) &&
<Col span={7} offset={1}>
<CourseSearchSelect
queryAll={true}
placeholder='请选择课程'
withGroup={true}
onSelect={(course) => { this.props.onChange('courseId', course.id) }}
defaultValue={courseId}
/>
</Col>
}
{
isNormal &&
<Col span={7} offset={1}>
<ClassSearchSelect
placeholder='请选择班级'
needName={true}
className={className}
courseId={courseId}
defaultValue={classId}
onSelect={(classesInfo) => { this.props.onChange('classId', classesInfo.classId);}}
/>
</Col>
}
{
isPotential &&
[
<Col span={7} offset={1} key="select-sale">
<StaticSelect
id="liveCourse_selectSale"
dataSource={salesData}
placeholder='请选择跟进人'
width={150}
defaultValue={saleId}
onSelect={(value) => {
this.props.onChange('saleId', value.id);
}}
/>
</Col>,
<Col span={7} offset={1} key="select-resource-type">
<StaticSelect
id="liveCourse_selectResourceType"
placeholder='请选择学员来源'
dataSource={resourceData}
defaultValue={resourceData[resourceType] ? resourceData[resourceType] : ''}
onSelect={(resourceTypeInfo) => { this.props.onChange('resourceType', resourceTypeInfo.id);}}
/>
</Col>
]
}
<span
className="icon iconfont"
onClick={() => {
Bus.trigger('resetSearchBar');
this.props.onReset();
}}
>
&#xe6a3;
</span>
</Row>
</div>
)
}
}
export default FilterContent;
/*
* @Author: 吴文洁
* @Date: 2020-08-07 16:28:49
* @LastEditors: 吴文洁
* @LastEditTime: 2020-09-27 19:35:32
* @Description: 选择学员-学员列表组件
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Table, Tooltip, Menu, Dropdown } from 'antd';
import Bus from '@/core/bus';
import { PageControl } from '@/components';
let resourceData = {};
_.map(window.RESOURCE, item => {
resourceData[item.code] = item.name
});
const isNewVersion = window.NewVersion;
class StudentList extends React.Component {
constructor(props) {
super(props);
this.state = {
showDegiest: false,
selectedRows: [],
prevSelectRows: props.savedSelectedRows || [],
savedSelectedRows: props.savedSelectedRows || []
}
}
parseColumns = () => {
const { query: { status }, type, studentType } = this.props;
// 是否是在读学员
const isNormal = (status === 'NORMAL' || status === 1);
// 是否是潜在学员
const isPotential = (status === 'POTENTIAL' || status === 2);
// 是否是历史学员
const isHistory = (status === 'HISTORY' || status === 4);
const columns = [
{
title: '姓名',
dataIndex: 'name',
width: 150,
render: (text, record) => {
const { studentBasicVO = {}, name } = record;
return studentBasicVO.name || name;
}
},
{
title: '手机号',
dataIndex: 'phone',
render: (text, record) => {
const { type } = this.props;
const { studentBasicVO = {}, phone, weChatStatus, wechatStatus } = record;
const { NewVersion, currentUserInstInfo: { teacherId } } = window;
return (
<div className="record__item">
{
!((!NewVersion && !teacherId) || (NewVersion && Permission.hasEduStudentPhone())) ?
(studentBasicVO.phone || phone).replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3') :
studentBasicVO.phone || phone
}
{
type !== 'videoCourse' &&
// 此处为了兼容4.0 和 5.0的扣课 不扣课学员的绑定微信
<Tooltip
title={`${(studentBasicVO.weChatStatus || weChatStatus || wechatStatus) ? '已绑定微信' : '未绑定微信'}`}
>
<span
className="iconfont icon"
style={(studentBasicVO.weChatStatus || weChatStatus || wechatStatus) ? {
color: '#00D20D',
fontSize: '16px',
marginLeft: 6
} : {
fontSize: '16px',
color:'#BFBFBF',
marginLeft: 6
}}
>&#xe68d;</span>
</Tooltip>
}
</div>
)
}
}
];
// 扣课时学员显示消耗课程和剩余课时
if (studentType === 'DEDUCTION') {
const { consumeStudentList } = this.props;
columns.push({
title: (
<div className="consumption-course">
<span className="text">消耗课程</span>
<Tooltip title="学员在这上课所消耗的课程">
<span className="icon iconfont">&#xe6f2;</span>
</Tooltip>
</div>
),
key: 'courseName',
dataIndex: 'courseName',
render: (val, record) => {
const { consumeStudentList } = this.props;
const { digestHourVOS, studentId } = record;
if (!digestHourVOS || !digestHourVOS.length) {
return <span className="digest-hour--empty">无可消耗课程</span>
} else if (digestHourVOS.length === 1) {
return <span className="course-name">{digestHourVOS[0].courseName}</span>
} else {
const { showDegiest, currentCourse } = this.state;
// 默认显示第一个课程包的名称
let _currentCourse = digestHourVOS[0];
// 判断学员是否被勾选了
const hasSelect = _.find(consumeStudentList, item => {
return item.studentId === studentId;
});
// 如果被勾选了,再判断选中的课时包是哪个
if (hasSelect) {
const hasSelectCourse = _.find(digestHourVOS, item => {
return item.courseId === hasSelect.classHourId
});
if (hasSelectCourse) {
_currentCourse = hasSelectCourse
}
}
// 选择课时包之后, 根据学员ID判断选择的是哪个学员的课时包
if (currentCourse && currentCourse.studentId === studentId) {
_currentCourse = currentCourse;
}
if (this.isDisabledRow(record)) {
return <span className="course-name">{_currentCourse.courseName}</span>
} else {
return (
<Dropdown
overlay={this.renderCourseMenu(digestHourVOS)}
placement="bottomCenter"
arrow
>
<div
className="digest-hour"
onMouseEnter={() => {
this.setState({ showDegiest: true });
}}
>
<span className="course-name">{_currentCourse.courseName}</span>
{
digestHourVOS.length > 1 && !this.isDisabledRow(record) &&
<span className={`icon iconfont ${showDegiest ? 'show' : 'hidden'}`}>&#xe6fa;</span>
}
</div>
</Dropdown>
)
}
}
}
}, {
title: '剩余课时',
key: 'leftLessons',
dataIndex: 'leftLessons',
render: (val, record) => {
const { consumeStudentList } = this.props;
const { digestHourVOS, studentId } = record;
if (!digestHourVOS || !digestHourVOS.length) {
return '-'
} else if (digestHourVOS.length === 1) {
return digestHourVOS[0].leftLessons;
} else {
const { currentCourse } = this.state;
let _currentCourse = digestHourVOS[0];
// 判断学员是否被选中了
const hasSelect = _.find(consumeStudentList, item => {
return item.studentId === studentId;
});
// 如果学员被勾选了,再判断该学员选择的课程包是哪个
if (hasSelect) {
const hasSelectCourse = _.find(digestHourVOS, item => {
return item.courseId === hasSelect.classHourId
});
if (hasSelectCourse) {
_currentCourse = hasSelectCourse
}
}
// 选择课时包之后, 根据学员ID判断选择的是哪个学员的课时包
if (currentCourse && currentCourse.studentId === studentId) {
_currentCourse = currentCourse;
}
return _currentCourse.leftLessons;
}
}
})
}
// 在读学员显示年级
if (type === 'videoCourse' && isNormal) {
columns.push({
title: '年级',
dataIndex: 'gradeName',
render: (text, record) => {
const { studentBasicVO = {}, gradeName } = record;
return studentBasicVO.gradeName || gradeName;
}
})
}
// 潜在学员显示跟进人和学员来源
if (isPotential) {
columns.push({
title: '跟进人',
dataIndex: 'saleName',
render: (val, record) => {
const { studentSaleVO = {}, saleName } = record;
return studentSaleVO.saleName || saleName;
}
}, {
title: '学员来源',
dataIndex: 'resourceType',
render: (val, record) => {
const { studentSaleVO = {}, resourceType } = record;
return resourceData[studentSaleVO.resourceType || resourceType];
}
});
}
// 历史学员显示结业时间和报读课程
if (isHistory) {
columns.push({
title: '结业时间',
key: 'graduationTime',
dataIndex: 'graduationTime',
width: 200,
render: (val, record) => {
const { studentHistoryVO = {}, graduationTime } = record;
return formatDate('YYYY-MM-DD', (studentHistoryVO.graduationTime || graduationTime))
}
}, {
title: '报读课程',
key: 'lostCourseName',
dataIndex: 'lostCourseName',
width: 150,
render: (val, record) => {
const { studentHistoryVO = {}, lostCourseName } = record;
return studentHistoryVO.lostCourseName || lostCourseName
}
});
}
return columns;
}
// 课程下拉选项
renderCourseMenu = (courseList) => {
return (
<Menu>
{
_.map(courseList, (item) => {
return (
<Menu.Item onClick={(e) => { this.handleSelectCourse(e, item)} }>
{ item.courseName }
</Menu.Item>
)
})
}
</Menu>
)
}
// 选择当前课程
handleSelectCourse = (e, currentCourse) => {
e.domEvent.stopPropagation();
this.setState({
currentCourse,
showDegiest: false,
}, () => {
const { consumeStudentList = [], studentIds, excludeIds } = this.props;
const { prevSelectRows } = this.state;
const { studentId, courseId, leftLessons, name, phone } = currentCourse;
let _consumeStudentList = consumeStudentList;
// 选完课时包之后,自动勾选当前学员
const _studentIds = [...studentIds, studentId];
const hasExist = _.find(consumeStudentList, item => {
return item.studentId === studentId
});
if (hasExist) {
_consumeStudentList = _.map(consumeStudentList, item => {
if (item.studentId === studentId) {
item.classHourId = courseId;
}
return item;
});
} else {
_consumeStudentList.push({
name,
phone,
studentId,
classHourId: courseId,
consumeHourNum: leftLessons
})
}
this.props.onSelect(_studentIds , _consumeStudentList, prevSelectRows);
});
}
handleSelect = (selectedRowKeys, selectedRows) => {
const { studentType, allstudentList, consumeStudentList } = this.props;
const studentIds = _.uniq([].concat(selectedRowKeys));
let allStudentIds = _.pluck(allstudentList, 'studentId');
let allSelectRows = [];
if (studentType === 'DEDUCTION') {
let { currentCourse, prevSelectRows } = this.state;
prevSelectRows = _.filter(prevSelectRows, (item) => {
return allStudentIds.indexOf(item.studentId) === -1
});
const savedSelectedRows = [...prevSelectRows, ...selectedRows];
allSelectRows = savedSelectedRows.map((item) => {
const { studentId, digestHourVOS = [], name, phone } = item;
// 如果该学员已经被选择了
const hasSelectItem = _.find(consumeStudentList, _item => {
return _item.studentId === item.studentId;
});
if (hasSelectItem) {
return hasSelectItem;
}
return {
name,
phone,
studentId,
classHourId: digestHourVOS[0].courseId,
consumeHourNum: digestHourVOS[0].leftLessons
}
});
this.setState({
selectedRows,
savedSelectedRows
}, () => {
this.props.onSelect(studentIds, allSelectRows, savedSelectedRows);
});
} else {
this.props.onSelect(studentIds);
}
}
isDisabledRow = (record) => {
// 扣课时的情况下, 无消耗课程的禁用
const {
after,
excludeIds,
studentType,
studentList = [],
consumeStudentList = []
} = this.props;
const { digestHourVOS } = record;
const hasDigestHours = digestHourVOS && digestHourVOS.length;
// 已经入库的学员不可再选择
const hasSelect = _.find(excludeIds, item => {
return item == record.studentId
});
let disabled = false;
// 扣课时学员课时为0的情况下禁止选择
if (studentType === 'DEDUCTION') {
// 判断是否已经在不扣课时里
if (_.find(studentList, item => item.studentId === record.studentId)) {
disabled = true;
} else if (!hasDigestHours || (after && !!hasSelect)) {
disabled = true;
}
} else {
if (_.find(consumeStudentList, item => item.studentId === record.studentId)) {
disabled = true;
} else if (after && !!hasSelect) {
disabled = true
}
}
return disabled
}
render() {
const {
after,
query,
totalCount,
studentIds,
allstudentList,
} = this.props;
const { prevSelectRows, selectedRows } = this.state;
const { current, size, pageNo, pageSize, } = query;
const rowSelection = {
selectedRowKeys: studentIds,
onChange: this.handleSelect,
getCheckboxProps: (record) => {
return {
disabled: this.isDisabledRow(record)
}
}
};
return (
<div className="student-list">
<Table
bordered
size={'small'}
rowKey={item => item.studentId}
dataSource={allstudentList}
columns={this.parseColumns()}
rowSelection={rowSelection}
onRow={record => ({
onClick: e => {
e.currentTarget
.getElementsByClassName("ant-checkbox-wrapper")[0]
.click();
}
})}
scroll={{ y: 350 }}
pagination={false}
/>
{
isNewVersion ?
<PageControl
size="small"
current={current - 1}
pageSize={size}
total={totalCount}
showSizeChanger={true}
toPage={(page) => {
this.state.prevSelectRows = this.state.savedSelectedRows;
this.props.onChange('current', page + 1);
}}
onShowSizeChange={(current, size) => {
this.props.onChange('size', size);
}}
/> :
<PageControl
size="small"
current={pageNo}
pageSize={pageSize}
total={totalCount}
showSizeChanger={true}
toPage={(page) => {
this.props.onChange('pageNo', page);
}}
onShowSizeChange={(pageNo, pageSize) => {
this.props.onChange('pageNo', pageSize);
}}
/>
}
</div>
)
}
}
export default StudentList;
/*
* @Author: 吴文洁
* @Date: 2020-08-07 16:23:27
* @LastEditors: 吴文洁
* @LastEditTime: 2020-09-27 19:33:20
* @Description: 选择学员组件
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
import React from 'react';
import { Modal, Radio, message } from 'antd';
import Bus from '@/core/bus';
import ShowTips from "@/components/ShowTips";
import FilterContent from './FilterContent';
import StudentList from './StudentList';
import './index.less';
const isNewVersion = window.NewVersion;
const DEFAULT_NEW_QUERY = {
size: 10,
current: 1,
status: 'NORMAL', // 学员类型, 默认显示在读学员
};
const DEFAULT_NEW_DEDUCTION_QUERY = {
size: 10,
current: 1,
status: 'NORMAL', // 学员类型, 默认显示在读学员
}
const DEFAULT_OLD_QUERY = {
pageNo: 0,
pageSize: 10,
status: 1,
}
const OLD_URL = {
1: 'api-b/b/student/list',
2: 'api-b/b/potential/list',
4: 'api-b/b/student/history/list'
}
class SelectStudent extends React.Component {
constructor(props) {
super(props);
const {
studentType,
// 扣课时学员列表
consumeStudentList = [],
// 不扣课时学员列表
studentList = [],
// 不扣课时学员ID列表(已经入库的)
excludeStudentIds = [],
// 扣课时学员ID列表(已经入库的)
excludeConsumeStudentIds = []
} = props;
const _studentList = studentType === 'DEDUCTION' ? consumeStudentList : studentList;
const excludeIds = studentType === 'DEDUCTION' ? excludeConsumeStudentIds : excludeStudentIds;
this.state = {
newQuery: { ...DEFAULT_NEW_QUERY },
oldQuery: { ...DEFAULT_OLD_QUERY } ,
newDeductionQuery: { ...DEFAULT_NEW_DEDUCTION_QUERY },
totalCount: 0,
allstudentList: [], // 学员列表
excludeIds, // 已经入库的学员
consumeStudentList: [].concat(consumeStudentList), // 扣课时学员列表
studentIds: _.pluck(_studentList, 'studentId'),
}
}
componentDidMount() {
this.handleFetchStudentList();
}
// 获取学员列表
handleFetchStudentList = () => {
// 5.0和4.0请求学员列表的接口不一样
if (isNewVersion) {
const { studentType } = this.props;
// 扣课时学员
if (studentType === 'DEDUCTION') {
this.handleFetchNewDeductionStudentList();
} else {
// 不扣课时学员
this.handleFetchNewStudentList();
}
} else {
this.handleFetchOldStudentList();
}
}
// 获取不扣课时学员列表
handleFetchNewStudentList = () => {
const { adminId, instId } = window.currentUserInstInfo;
const { studentType } = this.props;
const { newQuery } = this.state;
const {
current, size, status, courseId, saleId,
classId, teacherId, nameOrPhone, resourceType
} = newQuery;
const _query = {
size,
current,
status,
instId,
adminId,
expandRequest: {
courseId,
classId,
teacherId,
},
basicRequest: {},
filterRequest: {
saleId,
resourceType,
}
};
if ((nameOrPhone || '').match(/[^0-9]/)) {
_query.basicRequest.nameLike = nameOrPhone;
} else {
_query.basicRequest.phoneLike = nameOrPhone;
}
window.axios.Business('public/student/getLearningStudentPage', _query)
.then((res) => {
const { result = {} } = res;
const { records = [], total = 0 } = result;
const studentVOList = records;
let allstudentList = [];
allstudentList = _.map(studentVOList, item => {
const { studentBasicVO: { studentId } } = item;
return {
...item,
studentId,
key: studentId
}
});
this.setState({
allstudentList,
totalCount: Number(total),
});
});
}
// 获取扣课时学员列表
handleFetchNewDeductionStudentList = () => {
const { newDeductionQuery } = this.state;
const { size, current, courseId, classId, nameOrPhone } = newDeductionQuery;
const _query = {
size,
current,
classId,
courseId,
}
if ((nameOrPhone || '').match(/[^0-9]/)) {
_query.nameLike = nameOrPhone;
} else {
_query.phoneLike = nameOrPhone;
}
window.axios.Business('public/student/getStudentListWithUseAbleAccount', _query)
.then((res) => {
const { result = {} } = res;
const { records = [], total = 0 } = result;
const allstudentList = _.map(records, item => ({
...item,
key: item.studentId,
}));
this.setState({
allstudentList,
totalCount: Number(total),
})
});
}
handleFetchOldStudentList = () => {
const { oldQuery } = this.state;
const { studentType } = this.props;
const {
pageNo, pageSize, courseId, saleId, status,
classId, teacherId, nameOrPhone, resourceType
} = oldQuery;
const _query = {
pageNo,
pageSize,
saleId,
courseId,
classId,
teacherId,
nameOrPhone,
resourceType,
}
window.axios.post(OLD_URL[status], _query)
.then((res) => {
let studentVOList = [];
let totalCount = 0;
if (status !== 4) {
const { data = {} } = res || {};
studentVOList = data.studentVOList;
totalCount = data.totalCount;
} else {
studentVOList = res.data;
totalCount = res.totalCount;
}
const allstudentList = _.map(studentVOList, item => ({
...item,
key: item.studentId,
studentId: item.studentId
}));
this.setState({
totalCount,
allstudentList,
})
})
}
// 修改学员类型,清空搜索条件
handleChangeStatus = (e) => {
const { value } = e.target;
Bus.trigger('resetSearchBar');
this.setState({
newQuery: {
size: 10,
current: 1,
status: value,
},
oldQuery: {
pageNo: 0,
pageSize: 10,
status: value,
},
newDeductionQuery: {
size: 10,
current: 1
}
}, () => {
this.handleFetchStudentList();
})
}
handleChangeQuery = (field, value) => {
const { newQuery, oldQuery, newDeductionQuery } = this.state;
const _newQuery = {
...newQuery,
[field]: value
};
if (field === 'size') {
_newQuery.current = 1;
}
const _oldQuery = {
...oldQuery,
[field]: value
};
if (field === 'size') {
_oldQuery.pageNo = 0;
}
const _newDeductionQuery = {
...newDeductionQuery,
[field]: value
};
if (field === 'size') {
_newDeductionQuery.current = 1;
}
this.setState({
newQuery: {
..._newQuery,
},
oldQuery: {
..._oldQuery,
},
newDeductionQuery: {
..._newDeductionQuery,
}
}, () => {
this.handleFetchStudentList();
});
}
// 重置搜索条件
handleReset = () => {
const { newQuery, oldQuery } = this.state;
this.setState({
newQuery: {
...DEFAULT_NEW_QUERY,
status: newQuery.status
},
oldQuery: {
...DEFAULT_OLD_QUERY,
status: oldQuery.status
},
newDeductionQuery: {
...DEFAULT_NEW_DEDUCTION_QUERY
}
}, () => {
this.handleFetchStudentList();
})
}
// 勾选学员
handleSelect = (studentIds, consumeStudentList = [], savedSelectedRows) => {
const { studentType } = this.props;
const _studentIds = _.uniq([].concat(studentIds));
const _consumeStudentList = _.uniq([].concat(consumeStudentList));
this.setState({
savedSelectedRows,
studentIds: _studentIds,
consumeStudentList: studentType === 'DEDUCTION' ? _consumeStudentList : this.state.consumeStudentList
});
}
// 确定选择学员
handleSelectDone = () => {
const { studentIds, consumeStudentList, excludeIds, savedSelectedRows } = this.state;
if (!_.isEqual(studentIds, excludeIds)){
this.props.onSelect(studentIds, consumeStudentList, savedSelectedRows);
} else {
message.warning('请选择学员')
}
}
render() {
const {
type,
after,
showTabs,
studentType,
studentList,
savedSelectedRows
} = this.props;
const {
newQuery,
oldQuery,
newDeductionQuery,
totalCount,
studentIds,
excludeIds,
allstudentList,
consumeStudentList
} = this.state;
const { status: newStatus } = newQuery;
const { status: oldStatus } = oldQuery
const status = isNewVersion ? newStatus : oldStatus;
const query = isNewVersion ? studentType === 'DEDUCTION' ? newDeductionQuery : newQuery : oldQuery;
return (
<Modal
title="选择学员"
visible={true}
width={720}
onCancel={this.props.close}
className="livecourse__select-student-modal"
onOk={this.handleSelectDone}
>
{
showTabs &&
<Radio.Group
onChange={this.handleChangeStatus}
value={status}
>
<Radio.Button value={isNewVersion ? 'POTENTIAL' : 2}>潜在学员</Radio.Button>
<Radio.Button value={isNewVersion ? 'NORMAL' : 1}>在读学员</Radio.Button>
<Radio.Button value={isNewVersion ? 'HISTORY' : 4}>历史学员</Radio.Button>
</Radio.Group>
}
{
studentType === 'DEDUCTION' &&
<div className="show-tips">
<ShowTips message="只支持选择有剩余课时的在读学员" />
</div>
}
<FilterContent
type={type}
query={query}
studentType={studentType}
onReset={this.handleReset}
onChange={this.handleChangeQuery}
/>
<StudentList
type={type}
after={after}
query={query}
savedSelectedRows={savedSelectedRows}
studentType={studentType}
allstudentList={allstudentList}
totalCount={totalCount}
studentIds={studentIds}
excludeIds={excludeIds}
studentList={studentList}
consumeStudentList={consumeStudentList}
onSelect={this.handleSelect}
onChange={this.handleChangeQuery}
/>
</Modal>
)
}
}
export default SelectStudent;
\ No newline at end of file
.livecourse__select-student-modal {
.show-tips {
margin-bottom: 16px;
}
.ant-radio-group {
display: flex;
align-items: center;
justify-content: center;
}
.ant-table-bordered .ant-table-body {
border: none;
}
.xm-page-control {
padding-bottom: 0;
margin-bottom: 0;
}
.filter-content {
position: relative;
.iconfont {
position: absolute;
right: 4px;
top: 8px;
font-size: 14px;
line-height: 16px;
cursor: pointer;
}
}
.student-list {
padding-top: 4px;
.consumption-course {
display: flex;
align-items: center;
.text {
margin-right: 4px;
}
.iconfont {
cursor: pointer;
}
}
.digest-hour {
cursor: pointer;
display: flex;
.iconfont {
color: #666;
margin-left: 4px;
font-size: 14px;
&.show {
transform: rotate(90deg);
}
&.hidden {
transform: rotate(270deg);
}
}
&--empty {
color: #999;
cursor: default;
}
}
}
}
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-20 14:51:18
* @LastEditors: 吴文洁
* @LastEditTime: 2020-08-20 14:51:19
* @Description:
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
const dealTimeDuration = (time) => {
const diff = Math.floor(time % 3600);
let hours = Math.floor(time / 3600);
let mins = Math.floor(diff / 60);
let seconds = Math.floor(time % 60);
hours = hours < 10 ? "0" + hours : hours;
mins = mins < 10 ? "0" + mins : mins;
seconds = seconds < 10 ? "0" + seconds : seconds;
return hours + ":" + mins + ":" + seconds;
};
export default dealTimeDuration;
\ No newline at end of file
/*
* @Author: 吴文洁
* @Date: 2020-08-17 17:35:35
* @LastEditors: 吴文洁
* @LastEditTime: 2020-08-21 16:49:31
* @Description:
* @Copyright: 杭州杰竞科技有限公司 版权所有
*/
const hasExportPermission = (type) => {
// 互动班课导出权限
if (type === 'interactive') {
return Permission.hasInteractiveExport();
}
// 视频课导出权限
if (type === 'videoClass') {
return Permission.hasVideoClassExport();
}
// 大班直播导出权限
return Permission.hasBigLiveExport();
}
export default hasExportPermission;
......@@ -2,7 +2,7 @@
* @Author: 吴文洁
* @Date: 2020-04-29 10:26:32
* @LastEditors: zhangleyuan
* @LastEditTime: 2020-12-09 11:25:42
* @LastEditTime: 2020-12-09 15:00:48
* @Description: 内容线路由配置
*/
import EmployeesManagePage from '@/modules/store-manege/EmployeesManagePage';
......@@ -10,6 +10,7 @@ import personalInfoPage from '@/modules/personalInfo';
import UserManagePage from '@/modules/store-manege/UserManagePage';
import StoreDecorationPage from '@/modules/store-manege/StoreDecorationPage';
import CourseCatalogPage from '@/modules/store-manege/CourseCatalogPage'
import LiveCoursePage from '@/modules/course-manage/LiveCoursePage'
const mainRoutes = [
{
path: '/employees-manage',
......@@ -36,6 +37,11 @@ const mainRoutes = [
component:CourseCatalogPage,
name: '课程分类'
},
{
path: '/live-course',
component:LiveCoursePage,
name: '课程分类'
},
]
export default mainRoutes;
\ No newline at end of file
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