diff --git a/package-lock.json b/package-lock.json index 9a505de..13b2932 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5422,6 +5422,11 @@ "schema-utils": "^2.5.0" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "filesize": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", diff --git a/package.json b/package.json index 6db4a8e..c8b6e1a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "clipboard": "^2.0.10", "core-js": "^3.19.3", "element-ui": "^2.15.6", + "file-saver": "^2.0.5", "jquery": "^3.6.0", "js-cookie": "^3.0.1", "lib-flexible": "^0.3.2", diff --git a/src/assets/images/dataforward.png b/src/assets/images/dataforward.png new file mode 100644 index 0000000..6c75327 Binary files /dev/null and b/src/assets/images/dataforward.png differ diff --git a/src/components/TestPanel.vue b/src/components/TestPanel.vue index 721d413..088b22b 100644 --- a/src/components/TestPanel.vue +++ b/src/components/TestPanel.vue @@ -463,7 +463,20 @@ export default { type: 'warning', center: true }).then(() => { - this.submit() + // 如果全部都没运行直接点提交,则主动运行一个空代码。(不然会造成上次运行的结果,这次进来不运行直接提交的话,会无法取到运行结果) + if (!pointList.find(e => e.codeId)) { + this.$post(this.api.runPythonCode, { + code: '', + bcId: pointList[0].judgmentId, + cid: this.courseId, + projectId: this.projectId + }).then(({ codeId }) => { + this.$parent.workbench[0].codeId = codeId + this.submit() + }).catch(err => {}) + } else { + this.submit() + } }).catch(() => {}) }, // 提交 diff --git a/src/components/codemirror.vue b/src/components/codemirror.vue index 71d282b..fef0e0c 100644 --- a/src/components/codemirror.vue +++ b/src/components/codemirror.vue @@ -43,6 +43,7 @@
运行成功 + 导出结果
@@ -479,6 +480,11 @@ export default { downloadPic(i) { this.$refs['picLink' + i][0].click() }, + exportResult() { + var FileSaver = require('file-saver'); + var blob = new Blob([this.runResult], {type: "text/plain;charset=utf-8"}); + FileSaver.saveAs(blob, 'result.csv') + }, // 获取正式答案 getTips() { this.tipsVisible = true diff --git a/src/views/Data.vue b/src/views/Data.vue index 025ba89..376dce4 100644 --- a/src/views/Data.vue +++ b/src/views/Data.vue @@ -5,9 +5,7 @@
- - 导入数据 - + 导入数据 批量删除
@@ -40,6 +38,55 @@ + + + + + + + + +
+ + +
+ + +
+

+ 预览 +
+ + + + +
+
+
+ + 取 消 + 确 定 + +
@@ -58,7 +105,21 @@ export default { keyword: '', page: 1, pageSize: 10, - total: 0 + total: 0, + sourceVisible: false, + + importVisible: false, + defaultTypeActive: [], + defaultTypeChecked: [], + importLoading: false, + tablePromises: [], + importTypeList: [], + fieldData: [], + fieldHead: [], + tableName: '', + curId: '', + curExpand: '', + submited: false, }; }, components: { @@ -119,6 +180,7 @@ export default { ossFileName: file.ossFileName }).then(res => { this.$message.success('导入成功') + this.sourceVisible = false this.getData() }).catch(res => {}) }, @@ -177,6 +239,287 @@ export default { }, handleSelectionChange(val) { this.multipleSelection = val + }, + + + // 递归查询当前展开的分类下的子分类 + getMoreTable(list,id){ + list.map(n => { + if(n.id == id){ + this.getTable(n,1) + }else if(n.children && n.children.length){ + this.getMoreTable(n.children,id) + } + }) + }, + // 懒加载后新加载出来的数据可能是之前就存在于这个分类下了的,因此是要自动勾选的,所以在这里去拿这个分类下已经存在了的表的id去做一个重新勾选 + renderChecked(){ + let result = [] + let list = this.$refs.typeTree.getCheckedNodes() + list.map((n,i) => { + if(n.tableLen && n.tableLen > n.children.length){ + result = [...result,...n.children.map(n => n.id)] + }else{ + result.push(n.id) + } + }) + this.$refs.typeTree.setCheckedKeys(result) + }, + // 因为数据会有上万条的情况,一次加载出来dom太多了,所以用懒加载去处理 + loadType(e){ + clearTimeout(this.typeTimer) + let typeTree = this.$refs.typeTreeWrap + // 防抖 + this.typeTimer = setTimeout(() => { + this.typeTreeScrollTop = typeTree.scrollTop - this.typeTreeScrollTop + // 如果已经滚动到底部,则加载下面的数据 + if(this.typeTreeScrollTop > 0 && typeTree.scrollHeight - (typeTree.clientHeight + parseInt(typeTree.scrollTop)) < 10){ + this.getMoreTable(this.importTypeList,this.curExpand) + Promise.all(this.tablePromises).then(_ => { + this.renderChecked() + }) + } + },50) + }, + // 查询分类下的表 + getTable(n,isConcat){ + this.tablePromises.push(new Promise((resolve,reject) => { + this.$post(`${this.api.originalListById}?categoryId=${n.realId}&pageNum=${n.typeTreePage ? n.typeTreePage : 1}&pageSize=${this.typeTreeUnit}`).then(res => { + let list = res.list.records + list.map(n => { + n.label = n.showName + n.id = String(n.id) + // 已经存在的表,禁止再次勾选 + if(this.defaultTypeChecked.includes(n.id)){ + n.disabled = true + } + }) + // 如果是懒加载,则拼接 + if(isConcat){ + n.children = n.children.concat(list) + }else{ + n.children = list + n.tableLen = res.list.total + } + // 每次加载完,分页码数自动+1 + n.typeTreePage++ + resolve() + }).catch(res => { + reject() + }) + })) + }, + // 导入数据 + showData(){ + this.importLoading = true + this.importVisible = true + // 这里查的是原始分类,跟其他任何地方查分类的接口都不一样,不要混淆,如果要改这个地方,要跟后端沟通 + this.$post(this.api.originalList).then(res => { + // 处理id和label。因为这个树形要同时展示分类和表,所以name的key统一用label(分类和表的name的key不一样) + function handleId(data){ + data.map(n => { + n.realId = n.id + // 分类id跟表id可能会冲突,所以加上uuid + n.id = uuid.v4() + n.label = n.categoryName + if(n.children.length){ + handleId(n.children) + } + }) + } + handleId(res) + // 查询该分类下已勾选的表。pageSize传3000是防止查的表太少,无法选中分类,因为每个分类初始化的时候只查子下的20个表,后续可根据需求修改 + this.$post(`${this.api.getIdQueryTable}?categoryId=${this.categoryId}&showName=${this.keyword}&pageNum=1&pageSize=3000&updateTime=${this.updateTime ? this.updateTime : ''}`).then(res1 => { + let list = res1.pageList.records + this.defaultTypeChecked = list.map(n => n.copyId) // copyId为原始表id,通过这个id才能选中表 + // 递归处理分页 + const handleId = data => { + data.map(n => { + if(n.children.length){ + handleId(n.children) + }else{ + // 默认分页码数为1 + n.typeTreePage = 1 + this.getTable(n) + } + }) + } + handleId(res) + // 如果是已经勾选了的表,则禁止勾选 + function handleDisabled (list) { + list.map(e => { + if (e.children && e.children.length) { + if (e.children.every(e => e.disabled)) e.disabled = true + handleDisabled(e.children) + } + }) + } + Promise.all(this.tablePromises).then(_ => { + handleDisabled(res) + this.importTypeList = res + this.importLoading = false + }) + }).catch(res => { + this.importLoading = false + }) + }).catch(res => { + this.importLoading = false + }) + }, + // 查询表的字段 + getFields(){ + this.$get(`${this.api.staticPreview}?tableName=${this.tableName}`).then(res => { + let comment = res.comment + let fieldHead = [] + // id和操作时间这两个字段不用展示 + comment.map(n => { + n.field != 'id' && n.field != 'operation_time' && fieldHead.push(n) + }) + this.fieldHead = fieldHead + + let data = res.data + data.map(n => { + for(let i in n){ + // 如果是以+0000结尾的,就表明这个是时间,则转化为正常的时间格式 + if(typeof n[i] == 'string' && n[i].endsWith('+0000')) n[i] = this.formatDate('yyyy-MM-dd hh:mm:ss',new Date(n[i])) + } + }) + this.fieldData = data + }).catch(res => {}) + this.importVisible = true + }, + // 分类点击回调 + importTypeClick(data){ + // 如果点击的是表,则加载这个表下面的字段,分类则不用 + if(data.name){ + this.tableName = data.name + this.curId = data.id + this.getFields() + } + }, + // 分类展开的回调 + importTypeExpand(obj,node,com){ + this.curExpand = obj.id + this.renderChecked() + }, + // 关闭导入弹框回调 + closeImport(){ + this.$refs.typeTree.setCheckedKeys([]) + this.categorys = [] + }, + // 查询分类下的表id + getNames(list){ + const categoryId = Number(this.categoryId) + return new Promise((resolve,reject) => { + this.$get(`${this.api.getAllTableInfoByCategoryId}?categoryId=${list.join()}`).then(res => { + const list = res.tableInfo + list.map(e => { + delete e.id // id不用传给后端 + e.categoryId = categoryId + }) + this.categorys = list + resolve() + }).catch(res => {}) + }) + }, + // 保存成功后的操作 + saveSuccess() { + this.$message.success('导入成功') + this.getData() + this.importVisible = false + setTimeout(() => { + this.submited = false + },1000) + }, + // 导入表 + confirmImport(){ + if(this.submited) return false // 导入防抖标识 + const checked = this.$refs.typeTree.getCheckedNodes().filter(e => !e.disabled) // 勾选的数据 + // 如果没有选择任何数据 + if(!checked.length) return this.$message.warning('请选择数据') + this.submited = true + // 分类要调另一个接口去查询该分类下的表,所以,要先把分类id和表id分开,再一次性传分类id去查询表,再把查询出来的表跟另外勾选的表拼接,再保存。要做去重 + const typeIds = checked.filter(e => !e.name).map(e => e.realId) // 先筛选出分类id(分类没有name,表才有name),再获取分类真正的id(id作了自增处理,获取的id不是真正的id,realId才是这个分类真正的id) + const tableIds = checked.filter(e => e.name) // 筛选出表 + const categoryId = Number(this.categoryId) // 外面勾选的分类,即要导入数据的分类 + // 给表加categoryId + tableIds.map(e => { + delete e.id + e.categoryId = categoryId + }) + /** + * 根据某个值给数组对象去重 + * @param arr 要处理的数组 + * @param val 去重依据的值 + */ + function uniq (arr, val) { + const res = new Map() + return arr.filter(e => !res.has(e[val]) && res.set(e[val], 1)) + } + // 分类id,如果有分类id,则调接口去查询分类下的表,再新增,否则,直接拿表id去新增 + if (typeIds.length) { + this.getNames(typeIds).then(() => { + const allIds = [...this.categorys, ...tableIds] // 合并通过分类查询到的表id和另外勾选的表id + const data = uniq(allIds, 'name') + const dataList = [] + // 每个接口只新增2000个表,一次新增太多无法请求成功,这里做个分割 + for (let i = 0, len = data.length; i < len; i += 2000) { + dataList.push(data.slice(i, i + 2000)) + } + const promiseList = [] + // 如果切割成2000条一个item,超过6个item的话,就一次调太多次接口了,也有可能会请求失败,所以如果超过6个item,则再分两次去新增,后续如果数据又增加了,可以考虑做个二次分割,即先切割成2000个一个item,如果超过6个item,则再以6个item为单位切割。新增的时候就是每个接口新增2000个表,每次调6次新增,这6次调完后,再调另外的6次,以此循环 + if (dataList.length > 6) { + dataList.slice(0, 6).map(e => { + promiseList.push(new Promise((resolve,reject) => { + this.$post(this.api.saveTable, e).then(res => { + resolve() + }).catch(res => { + reject() + }) + })) + }) + Promise.all(promiseList).then(res => { + dataList.slice(6).map(e => { + promiseList.push(new Promise((resolve,reject) => { + this.$post(this.api.saveTable, e).then(res => { + resolve() + }).catch(res => { + reject() + }) + })) + }) + Promise.all(promiseList).then(res => { + this.saveSuccess() + }).catch(err => { + this.submited = false + }) + }).catch(err => { + this.submited = false + }) + } else { + dataList.map(e => { + promiseList.push(new Promise((resolve,reject) => { + this.$post(this.api.saveTable, e).then(res => { + resolve() + }).catch(res => { + reject() + }) + })) + }) + Promise.all(promiseList).then(res => { + this.saveSuccess() + }).catch(err => { + this.submited = false + }) + } + }) + } else { // 保存表 + this.$post(this.api.saveTable, tableIds).then(res => { + this.saveSuccess() + }).catch(res => { + this.submited = false + }) + } } } }; @@ -211,4 +554,32 @@ export default { cursor: pointer; } } +/deep/.source-list { + display: flex; + justify-content: space-between; + li { + display: inline-flex; + justify-content: center; + align-items: center; + height: 100px; + border: 1px solid #ccc; + border-radius: 8px; + cursor: pointer; + img { + width: 90%; + } + &:hover { + border-color: #007eff; + } + } + .upload { + width: 48%; + .el-upload { + width: 100%; + &:focus { + color: #007eff; + } + } + } +} \ No newline at end of file