diff --git a/public/react/package.json b/public/react/package.json index 6828a43c4..bf0b5cce0 100644 --- a/public/react/package.json +++ b/public/react/package.json @@ -78,7 +78,6 @@ "react-dev-utils": "^5.0.0", "react-dom": "^16.9.0", "react-hot-loader": "^4.0.0", - "react-infinite-scroller": "^1.2.4", "react-loadable": "^5.3.1", "react-monaco-editor": "^0.25.1", "react-player": "^1.11.1", @@ -176,6 +175,7 @@ "compression-webpack-plugin": "^1.1.12", "concat": "^1.0.3", "happypack": "^5.0.1", + "mockjs": "^1.1.0", "node-sass": "^4.12.0", "reqwest": "^2.0.5", "webpack-bundle-analyzer": "^3.0.3", diff --git a/public/react/src/AppConfig.js b/public/react/src/AppConfig.js index 12e0043d5..cf813573c 100644 --- a/public/react/src/AppConfig.js +++ b/public/react/src/AppConfig.js @@ -82,8 +82,10 @@ export function initAxiosInterceptors(props) { // proxy = "https://testeduplus2.educoder.net" //proxy="http://47.96.87.25:48080" proxy="https://pre-newweb.educoder.net" + proxy="https://test-newweb.educoder.net" + // proxy="https://test-jupyterweb.educoder.net" // proxy="https://test-newweb.educoder.net" - proxy="https://test-jupyterweb.educoder.net" + // proxy="https://test-jupyterweb.educoder.net" //proxy="http://192.168.2.63:3001" // 在这里使用requestMap控制,避免用户通过双击等操作发出重复的请求; diff --git a/public/react/src/modules/paths/Index.js b/public/react/src/modules/paths/Index.js index 7a1c3d301..67ef9125b 100644 --- a/public/react/src/modules/paths/Index.js +++ b/public/react/src/modules/paths/Index.js @@ -17,10 +17,14 @@ const PathsNew = Loadable({ loader: () => import('./PathNew'), loading:Loading, }) +// const Statistics = Loadable({ +// loader: () => import('./SchoolStatistics/Statistics'), +// loading:Loading +// }) const Statistics = Loadable({ - loader: () => import('./SchoolStatistics/Statistics'), - loading:Loading -}) + loader: () => import('./statics'), + loading: Loading +}); const ShixunPaths = Loadable({ loader: () => import('./ShixunPaths'), diff --git a/public/react/src/modules/paths/statics/DisplayTableData.js b/public/react/src/modules/paths/statics/DisplayTableData.js new file mode 100644 index 000000000..f59dd719d --- /dev/null +++ b/public/react/src/modules/paths/statics/DisplayTableData.js @@ -0,0 +1,80 @@ +/* + * @Description: + * @Author: tangjiang + * @Github: + * @Date: 2020-01-14 13:39:12 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 16:30:05 + */ +import './index.scss'; +import React, { useRef, useState, useEffect } from 'react'; +import { Table } from 'antd'; +import ReactDom from 'react-dom'; + +const DisplayTableData = (props) => { + const {columns, datas, fetchData, total} = props; + let tableEl = useRef(null); + const [loading, setLoading] = useState(false); + + const renderFooter = (obj = {}) => { + const {course_count, student_count, choice_shixun_num, choice_shixun_frequency, total} = obj; + if (!obj) return '' + else { + return ( + + ) + } + } + + useEffect(() => { + const table = ReactDom.findDOMNode(tableEl); + // console.log(table); + const tableBody = table.querySelector('.ant-table-body'); + let _scrollTop = 0;//保存上次滚动距离 + let isRun = false;//是否执行查询 + tableBody.addEventListener('scroll', () => { + if(tableBody.scrollTop === 0 ){ + _scrollTop = 0; + } + // 上一次滚动高度与当前滚动高度不同则是纵向滚动 + if (_scrollTop !== tableBody.scrollTop) { + //是否滑动到距离底部40px的位置 + const scorll = _scrollTop >= tableBody.scrollHeight-tableBody.clientHeight-40; + //isRun为true时 代表已经执行查询 + if(isRun && scorll){ + return; + } + //_scrollTop < tableBody.scrollTop 判断是否向下滑动 + isRun = _scrollTop < tableBody.scrollTop && scorll; + //保存当前滚动位置 + _scrollTop = tableBody.scrollTop; + if (isRun) { + fetchData && fetchData(); + } + } + }) + }, []); + + return ( + record.id} + columns={columns} + dataSource={datas} + pagination={false} + loading={loading} + scroll={{y: 500}} + ref={(ref)=>tableEl=ref} + footer={total ? () => renderFooter(total) : ''} + /> + ); +} + +export default DisplayTableData; \ No newline at end of file diff --git a/public/react/src/modules/paths/statics/StaticNumberAndTxt.js b/public/react/src/modules/paths/statics/StaticNumberAndTxt.js new file mode 100644 index 000000000..e061bdc80 --- /dev/null +++ b/public/react/src/modules/paths/statics/StaticNumberAndTxt.js @@ -0,0 +1,43 @@ +/* + * @Description: 数字及文字提示 + * @Author: tangjiang + * @Github: + * @Date: 2020-01-10 10:26:57 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-10 11:15:28 + */ +import './index.scss'; +import React from 'react'; +import { Tooltip } from 'antd'; +const numberal = require('numeral'); + +const StaticNumberAndTxt = ({ + count = 0, // 总数 + txt, // 文字描述 + type = 'tishi1', // 字体类型 + desc // 描述信息 +}) => { + + const formatNumber = (value, format = '0,0') => { + return numberal(value).format(format); + } + + const _classes = `iconfont icon-${type} icon`; + return ( +
+ {formatNumber(count)} + + {txt} + + + + +
+ ); +} + +export default StaticNumberAndTxt; diff --git a/public/react/src/modules/paths/statics/index.js b/public/react/src/modules/paths/statics/index.js new file mode 100644 index 000000000..37cc44df3 --- /dev/null +++ b/public/react/src/modules/paths/statics/index.js @@ -0,0 +1,339 @@ +/* + * @Description: 实践课程统计页面 + * @Author: tangjiang + * @Github: + * @Date: 2020-01-10 09:33:45 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 17:01:32 + */ +import './index.scss'; +import React, { useEffect } from 'react'; +import StaticNumberAndTxt from './StaticNumberAndTxt'; +import DisplayTableData from './DisplayTableData'; +import { Tabs, Tooltip } from 'antd'; +import { connect } from 'react-redux'; +import moment from 'moment'; +import actions from '../../../redux/actions'; +const { TabPane } = Tabs; +const App = (props) => { + + const { + subject_info, + other_info, + total, + staticList, + changeParams, + initTotal + } = props; + // const [datas, setDatas] = useState([]); + // const [sortedInfo, setSortedInfo] = useState({}); + // console.log(props); + const {pathId} = props.match.params; + const columns = [ + { + title: '序号', + dataIndex: 'id', + key: 'id', + render: (text, record, i) => i + 1, + width: 100, + align: 'center' + }, + { + title: '使用单位', + // key: 'school_name', + dataIndex: 'school_name', + // width: 300, + className: 'overflow_hidden', + align: 'center' + }, + { + // title: '使用课堂/个', + title: () => (使用课堂), + // key: 'course_count', + width: 150, + dataIndex: 'course_count', + align: 'center', + sorter: (a, b) => a.course_count - b.course_count, + // sortOrder: sortedInfo.columnKey === 'age' && sortedInfo.order + // sorter: (a, b) => true, + // sorter: (a, b) => a.age - b.age + }, + { + title: () => (课堂学生), + // key: 'student_count', + width:150, + dataIndex: 'student_count', + align: 'center', + sorter: (a, b) => a.student_count - b.student_count, + // sorter: (a, b) => a.age - b.age + }, + { + title: () => (选用实训/个), + width: 150, + // key: 'choice_shixun_num', + dataIndex: 'choice_shixun_num', + align: 'center', + sorter: (a, b) => a.choice_shixun_num - b.choice_shixun_num, + // sorter: (a, b) => a.age - b.age + }, + { + title: () => (选用实训/次), + width: 150, + // key: 'choice_shixun_frequency', + dataIndex: 'choice_shixun_frequency', + align: 'center', + sorter: (a, b) => a.choice_shixun_frequency - b.choice_shixun_frequency, + // sorter: (a, b) => a.bbb - b.bbb + } + ]; + const sxColumns = [ + { + title: '序号', + dataIndex: 'id', + render: (text, record, i) => i + 1, + width: 60, + align: 'center' + }, { + title: '章节', + dataIndex: 'stage', + width: 80, + align: 'center' + }, + { + title: '实训名称', + dataIndex: 'shixun_name', + align: 'center', + // ellipsis: true + }, + { + title: '关卡数', + dataIndex: 'challenge_count', + width: 100, + align: 'center' + }, + { + title: '使用课堂', + dataIndex: 'course_count', + width: 110, + align: 'center', + sorter: (a, b) => a.course_count - b.course_count + }, + { + title: '使用单位', + dataIndex: 'school_count', + width: 110, + align: 'center', + sorter: (a, b) => a.school_count - b.school_count + }, + { + title: '使用人数', + dataIndex: 'used_count', + width: 110, + align: 'center', + sorter: (a, b) => a.used_count - b.used_count + }, + { + title: '通关人数', + dataIndex: 'passed_count', + width: 110, + align: 'center', + sorter: (a, b) => a.passed_count - b.passed_count + }, + { + title: '评测次数', + dataIndex: 'evaluate_count', + width: 110, + align: 'center', + sorter: (a, b) => a.evaluate_count - b.evaluate_count + }, + { + title: '通关平均时间', + dataIndex: 'passed_ave_time', + width: 140, + align: 'center', + render: (text) => (text && moment(text).format('HH:mm:ss')) || '-', + sorter: (a, b) => a.passed_ave_time - b.passed_ave_time + } + ]; + const stColumns = [ + { + title: '序号', + dataIndex: 'id', + render: (text, record, i) => i + 1, + width: 60, + align: 'center' + }, + { + title: '姓名', + dataIndex: 'username', + align: 'center' + }, + { + title: '通关实训数', + dataIndex: 'passed_myshixun_count', + align: 'center', + sorter: (a, b) => a.passed_myshixun_count - b.passed_myshixun_count + }, + { + title: '完成关卡', + dataIndex: 'passed_games_count', + align: 'center', + sorter: (a, b) => a.passed_games_count - b.passed_games_count + }, + { + title: '代码行', + dataIndex: 'code_line_count', + align: 'center', + sorter: (a, b) => a.code_line_count - b.code_line_count + }, + { + title: '评测次数', + dataIndex: 'evaluate_count', + align: 'center', + sorter: (a, b) => a.evaluate_count - b.evaluate_count + }, + { + title: '所用时间', + dataIndex: 'cost_time', + align: 'center', + render: (text) => (text && moment(text).format('HH:mm:ss')) || '-', + sorter: (a, b) => a.cost_time - b.cost_time + } + ]; + + useEffect(() => { + changeParams({ + page: 1 + }); + pathId && staticList(pathId); + }, []); + + const handleFetchData = () => { + pathId && staticList(pathId); + } + + const { + study_count, + course_study_count, + initiative_study, + passed_count, + course_used_count, + school_used_count + } = subject_info; + + const maps = { + 1: 'subject_info', // 实践课程使用情况 + 2: 'shixun_info', // 实训使用情况 + 3: 'user_info' // 用户使用情况 + }; + + const handleTabChange = (key) => { + const type = maps[+key]; + // console.log(type); + const params = { + page: 1, + type: type + } + // 恢复初始值 + changeParams(params); + initTotal(); + pathId && staticList(pathId); + } + + return ( +
+
+
+
+
+ 学习统计 + Android综合实训之物联网移动应用 +
+
+ + + + + + +
+
+
+ + + + + + + + + + + + +
+
+
+
+ ); +} + +const mapStateToProps = (state) => { + const { staticReducer: {subject_info, other_info, total} } = state; + return { + subject_info, + other_info, + total + } +}; + +const mapDispatchToProps = (dispatch) => ({ + staticList: (id) => dispatch(actions.staticList(id)), + changeParams: (params) => dispatch(actions.changeParams(params)), + initTotal: () => dispatch(actions.initTotal()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(App); +// export default App; diff --git a/public/react/src/modules/paths/statics/index.scss b/public/react/src/modules/paths/statics/index.scss new file mode 100644 index 000000000..33c65136a --- /dev/null +++ b/public/react/src/modules/paths/statics/index.scss @@ -0,0 +1,156 @@ +.static_wrap { + .static_section_header, + .static_section_table{ + background-color: #fff; + border-radius: 5px; + padding: 30px 20px 0; + margin-top: 20px; + } + + .static_section_table{ + margin-bottom: 140px; + padding-top: 5px; + } + + .static_section_header{ + .header_title{ + line-height: 1; + .title-p, + .title-sub{ + display: inline-block; + vertical-align: bottom; + color: #303133; + } + + .title-p{ + position: relative; + font-size: 20px; + height: 20px; + line-height: 1; + font-weight: bold; + + &::before{ + position: absolute; + content: ''; + border-left: 1px solid rgba(192,196,204,1); + right: -10px; + top: 2px; + bottom: 0px; + margin-left: 10px; + } + } + .title-sub{ + margin-left: 20px; + font-size: 16px; + max-width: 1000px; + overflow: hidden; + text-overflow:ellipsis; + white-space: nowrap; + vertical-align: bottom; + } + } + + .header-number{ + height: 158px; + } + .header-flex{ + display: flex; + justify-content: space-around; + align-items: center; + + .static-flex-item{ + display: flex; + flex-direction: column; + justify-content: center; + .item-count{ + font-size: 24px; + color: #4CACFF; + font-weight: bold; + } + .item-txt{ + font-size: 14px; + line-height: 1.5; + text-align: center; + color: #909399; + margin-top: 20px; + .icon{ + margin-left: 5px; + font-size: 16px !important; + } + } + } + } + } + // .static_table{ + // // .ant-table-header{ + // // overflow: hidden !important; + // // margin-bottom: 0px !important; + // // } + // // .ant-table-row-cell-break-word{ + // // background: rgba(241,248,255,1) !important; + // // } + + // // .overflow_hidden{ + // // max-width: 280px; + // // overflow: hidden; + // // text-overflow:ellipsis; + // // white-space: nowrap; + // // } + // } + .static_table{ + .ant-table-header{ + margin-bottom: 0px !important; + overflow: hidden !important; + } + .ant-table-thead{ + th{ + background: rgba(241,248,255,1); + + } + .ant-table-column-title{ + color: #303133; + font-weight: bold; + } + } + .ant-table-tbody tr:nth-child(2n) { + td{ + background: rgba(241,248,255,.4); + } + } + .ant-table-tbody tr td{ + color: #303133; + } + } + + .ant-table-footer{ + background-color: rgba(241,248,255,1); + padding: 16px 0px; + } + .footer_list{ + display: flex; + // background: #fff; + box-sizing: border-box; + text-align: center; + + li{ + color: #303133; + } + // border-top: 1px solid green; + .footer_item{ + width: 150px; + } + .footer_item:not(:first-child) { + padding-right: 10px; + } + .footer-total{ + width: 100px; + } + .footer_name{ + flex: 1; + } + } +} + +.tool-clazz{ + max-width: 200px !important; +} diff --git a/public/react/src/modules/paths/statics/mockData.js b/public/react/src/modules/paths/statics/mockData.js new file mode 100644 index 000000000..693bb051c --- /dev/null +++ b/public/react/src/modules/paths/statics/mockData.js @@ -0,0 +1,99 @@ +/* + * @Description: 模拟数据 + * @Author: tangjiang + * @Github: + * @Date: 2020-01-11 10:55:33 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 09:11:36 + */ +import { random } from 'lodash'; + +const getGuid = () => + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + /* eslint-disable */ + let r = (Math.random() * 16) | 0, + v = c == 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + +const fetchData = (startIndex = 0) => + new Promise(resolve => { + setTimeout(() => { + resolve( + startIndex >= 500 // 总共只有500条数据 + ? [] + : Array.from({ length: 50 }).map((_, i) => { + // 每次返回100条 + const index = startIndex + i; + return { + key: getGuid(), + index: `${index}`, + name: '湖南长沙中南大学湖南长沙中南大学湖南长沙中南大学湖南长沙中南大学湖南长沙中南大学', + age: i+1, + address: 'New York No. ', + address2: 'address2', + bbb: 'address3' + }; + }), + ); + }, random(0, 1.0) * 1000); + }); + +const columns = [ + { + title: '序号', + dataIndex: 'index', + render: text => text + 1, + width: 80, + align: 'center' + }, + { + title: '使用单位', + dataIndex: 'name', + width: 300, + className: 'overflow_hidden', + align: 'center' + }, + { + title: '使用课堂/个', + width: 200, + dataIndex: 'age', + align: 'center', + // sorter: (a, b) => a.age - b.age + }, + { + title: '课堂学生/个', + width: 200, + dataIndex: 'address', + align: 'center', + // sorter: (a, b) => a.age - b.age + }, + { + title: '选用实训/个', + width: 200, + dataIndex: 'address2', + align: 'center', + // sorter: (a, b) => a.age - b.age + }, + { + title: '选用实训/个', + width: 200, + dataIndex: 'bbb', + align: 'center', + // sorter: (a, b) => a.bbb - b.bbb + } +]; + +const sumData = [ + { + index: '合计', + key: 6, + name: 'Disabled User', + age: 99, + address: 'Sidney No.', + address2: 'address2', + bbb: 'address3', + } +]; + +export { columns, fetchData, sumData }; diff --git a/public/react/src/redux/actions/actionTypes.js b/public/react/src/redux/actions/actionTypes.js index efe3ff35c..e3c2f65a9 100644 --- a/public/react/src/redux/actions/actionTypes.js +++ b/public/react/src/redux/actions/actionTypes.js @@ -88,7 +88,11 @@ const types = { CHANGE_COMMENT_PAGINATION_PARAMS: 'CHANGE_COMMENT_PAGINATION_PARAMS', // 改变分页 /** tpi */ SHOW_OR_HIDE_TPI_TEST_CASE: 'SHOW_OR_HIDE_TPI_TEST_CASE', // 显示或隐藏tpi测试集弹框 - IS_COLLAPSE_TEST_CASE: 'IS_COLLAPSE_TEST_CASE' // 是否展开测试集 + IS_COLLAPSE_TEST_CASE: 'IS_COLLAPSE_TEST_CASE', // 是否展开测试集 + /** 统计 */ + GET_STATIC_INFO: 'GET_STATIC_INFO', + CHANGE_STATIC_PARAMS: 'CHANGE_STATIC_PARAMS', + CHANGE_STATIC_TOTAL: 'CHANGE_STATIC_TOTAL' } export default types; diff --git a/public/react/src/redux/actions/index.js b/public/react/src/redux/actions/index.js index 0376b529d..f7b7a41d9 100644 --- a/public/react/src/redux/actions/index.js +++ b/public/react/src/redux/actions/index.js @@ -103,6 +103,12 @@ import { isCollpaseTsetCase } from './tpi'; +import { + staticList, + changeParams, + initTotal +} from './static'; + export default { toggleTodo, getOJList, @@ -181,5 +187,9 @@ export default { changePagination, // tpi showOrHideTpiTestCase, - isCollpaseTsetCase + isCollpaseTsetCase, + // 统计 + staticList, + changeParams, + initTotal } \ No newline at end of file diff --git a/public/react/src/redux/actions/static.js b/public/react/src/redux/actions/static.js new file mode 100644 index 000000000..3159d35c2 --- /dev/null +++ b/public/react/src/redux/actions/static.js @@ -0,0 +1,41 @@ +/* + * @Description: + * @Author: tangjiang + * @Github: + * @Date: 2020-01-14 09:44:02 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 17:02:45 + */ +import types from "./actionTypes"; +import { fetchStaticList } from "../../services/staticService"; + +export const staticList = (id) => { + return (dispatch, getState) => { + const { params, total_count, other_info } = getState().staticReducer; + + if (total_count !== 0 && total_count === other_info.length) return; + fetchStaticList(id, params).then(res => { + // console.log('统计数据=====>>>>>', res); + const {data} = res; + if (data.status === 0) { + dispatch({ + type: types.GET_STATIC_INFO, + payload: data.data + }); + } + }); + } +}; + +export const changeParams = (params) => { + return { + type: types.CHANGE_STATIC_PARAMS, + payload: params + } +} + +export const initTotal = () => { + return { + type: types.CHANGE_STATIC_TOTAL + } +} diff --git a/public/react/src/redux/reducers/index.js b/public/react/src/redux/reducers/index.js index 7c9601d52..6506cf584 100644 --- a/public/react/src/redux/reducers/index.js +++ b/public/react/src/redux/reducers/index.js @@ -16,6 +16,7 @@ import userReducer from './userReducer'; import jupyterReducer from './jupyterReducer'; import commentReducer from './commentReducer'; import tpiReducer from './tpiReducer'; +import staticReducer from './staticReducer'; export default combineReducers({ testReducer, @@ -26,5 +27,6 @@ export default combineReducers({ userReducer, jupyterReducer, commentReducer, - tpiReducer + tpiReducer, + staticReducer }); diff --git a/public/react/src/redux/reducers/staticReducer.js b/public/react/src/redux/reducers/staticReducer.js new file mode 100644 index 000000000..7d2202d03 --- /dev/null +++ b/public/react/src/redux/reducers/staticReducer.js @@ -0,0 +1,69 @@ +/* + * @Description: 统计 + * @Author: tangjiang + * @Github: + * @Date: 2020-01-14 09:34:49 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 15:49:55 + */ +import types from "../actions/actionTypes"; + +// const maps = { +// 1: 'shixun_info', // 实训使用情况 +// 2: 'user_info', // 用户使用情况 +// 3: 'subject_info' // 实践课程使用情况 +// } +const initalState = { + subject_info: {}, + other_info: [], + total_count: 0, + total: {}, + params: { + // sort_by: '', + // sort_direction: 'desc', // desc || asc + limit: 20, // 一页多少条 + page: 1, // 第几页 + type: 'subject_info' // 类型: 实训 shixun_info, + } +}; + +// const getGuid = () => +// 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { +// /* eslint-disable */ +// let r = (Math.random() * 16) | 0, +// v = c == 'x' ? r : (r & 0x3) | 0x8; +// return v.toString(16); +// }); + +const staticReducer = (state = initalState, action) => { + const { payload = {}, type } = action; + const {subject_info, other_info = [], total = {}, total_count} = payload; + switch (type) { + case types.GET_STATIC_INFO: + return { + ...state, + subject_info, + other_info: state.other_info.concat(other_info), + total, + total_count, + params: Object.assign({}, state.params, { page: state.params.page + 1 }) + } + case types.CHANGE_STATIC_PARAMS: { + return { + ...state, + params: Object.assign({}, state.params, payload) + }; + } + case types.CHANGE_STATIC_TOTAL: { + return { + ...state, + other_info: [], + total: {} + } + } + default: + return state; + } +} + +export default staticReducer; diff --git a/public/react/src/services/staticService.js b/public/react/src/services/staticService.js new file mode 100644 index 000000000..23e4f1b93 --- /dev/null +++ b/public/react/src/services/staticService.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +/* + * @Description: + * @Author: tangjiang + * @Github: + * @Date: 2020-01-14 09:40:53 + * @LastEditors : tangjiang + * @LastEditTime : 2020-01-14 10:47:19 + */ +export async function fetchStaticList (id, params) { + const url = `/paths/${id}/statistics_info.json`; + return axios.get(url, { params }); +}