作业批阅:附件预览

This commit is contained in:
白了个白 2024-09-09 15:50:55 +08:00
parent 0c506a09f2
commit 7cb84ffe37
9 changed files with 464 additions and 34 deletions

View File

@ -34,10 +34,13 @@
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"element-plus": "^2.7.6", "element-plus": "^2.7.6",
"fabric": "^5.3.0", "fabric": "^5.3.0",
"im_electron_sdk": "^8.0.5904",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"jsondiffpatch": "0.6.0", "jsondiffpatch": "0.6.0",
"im_electron_sdk": "^8.0.5904",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pdfjs-dist": "4.4.168", "pdfjs-dist": "4.4.168",
"pinia": "^2.1.7", "pinia": "^2.1.7",

View File

@ -39,6 +39,15 @@ export function updateClassworkeval(data) {
}) })
} }
// 修改classworkdata
export function updateClassworkdata(data) {
return request({
url: '/education/classworkdata',
method: 'put',
data: data
})
}

View File

@ -0,0 +1,118 @@
<script setup name="ReFilePreview">
import "@vue-office/docx/lib/index.css";
import "@vue-office/excel/lib/index.css";
import { defineAsyncComponent, defineProps, onMounted } from "vue";
// import type { FileProps } from "@/components/RefilePreview/types";
import { useHooks } from "@/components/refile-preview/useReadFile";
import VueOfficeDocx from '@vue-office/docx'
import VueOfficeExcel from '@vue-office/excel'
import VueOfficePdf from '@vue-office/pdf'
// const VueOfficeDocx = defineAsyncComponent(() => import("@vue-office/docx"));
// const VueOfficeExcel = defineAsyncComponent(() => import("@vue-office/excel"));
// const VueOfficePdf = defineAsyncComponent(() => import("@vue-office/pdf"));
// const RePlayer = defineAsyncComponent(
// () => import("@/components/RePlayer/index.vue")
// );
const props = defineProps({
name: {
type: String,
default: ""
},
type: {
type: String,
default: ""
},
fileType: {
type: String,
default: ""
},
raw: () => new File([], ""),
filePath: {
type: String,
default: ""
},
textContent:{
type: String,
default: ""
}
});
const {
excelOptions,
src,
filePreviewRef,
renderedHandler,
errorHandler,
renderTheFile,
isImage,
isVideo,
isText,
isAudio
} = useHooks(props);
onMounted(() => {
renderTheFile(); //
});
defineExpose({
filePreviewRef
});
</script>
<template>
<div ref="filePreviewRef" class="file-preview">
<vue-office-docx
v-if="props.fileType === 'docx' || props.fileType === 'doc'"
:src="props.filePath"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-excel
v-if="props.fileType === 'xlsx' || props.fileType === 'xls'"
:src="props.filePath"
width="100%"
height="100%"
:auto-resize="true"
:enable-scrollbars="true"
:options="excelOptions"
@rendered="renderedHandler"
@error="errorHandler"
/>
<vue-office-pdf
v-if="props.fileType === 'pdf'"
:src="props.filePath"
@rendered="renderedHandler"
@error="errorHandler"
/>
<el-image
v-if="isImage(props.fileType)"
:preview-teleported="true"
fit="cover"
class="w-[200px] align-left"
:src="props.filePath"
title="点击查看大图"
:preview-src-list="[src]"
/>
<video v-if="isVideo(props.fileType)" :src="props.filePath" style="width: 400px; height: 400px;" controls/>
<div v-if="isText(props.fileType)">
<pre v-html="props.textContent" />
</div>
<audio v-if="isAudio(props.fileType)" :src="props.filePath" controls />
</div>
</template>
<style lang="scss" scoped>
.file-preview {
width: 90%;
height: 60vh;
overflow: auto;
text-align: left;
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,7 @@
export interface FileProps {
id?: number;
type?: string;
fileType?: string;
raw?: File;
filePath?: string;
}

View File

@ -0,0 +1,261 @@
import type { FileProps } from "@/components/refile-preview/types";
import { onUnmounted, ref } from "vue";
import axios from "axios";
export function useHooks(props: FileProps) {
const excelOptions = {
xls: props.fileType !== "xlsx", //预览xlsx文件设为false预览xls文件设为true
minColLength: 0, // excel最少渲染多少列如果想实现xlsx文件内容有几列就渲染几列可以将此值设置为0.
minRowLength: 0, // excel最少渲染多少行如果想实现根据xlsx实际函数渲染可以将此值设置为0.
widthOffset: 10, //如果渲染出来的结果感觉单元格宽度不够,可以在默认渲染的列表宽度上再加 Npx宽
heightOffset: 10, //在默认渲染的列表高度上再加 Npx高
beforeTransformData: workbookData => {
return workbookData;
}, //底层通过exceljs获取excel文件内容通过该钩子函数可以对获取的excel文件内容进行修改比如某个单元格的数据显示不正确可以在此自行修改每个单元格的value值。
transformData: workbookData => {
return workbookData;
} //将获取到的excel数据进行处理之后且渲染到页面之前可通过transformData对即将渲染的数据及样式进行修改此时每个单元格的text值就是即将渲染到页面上的内容
};
const src = ref();
const filePreviewRef = ref();
/**
*
*/
const renderedHandler = () => {
console.log("渲染完成");
};
/**
*
* @param e
*/
const errorHandler = e => {
console.log("渲染失败", e);
};
/**
*
*/
async function renderTheFile() {
if (props.type === "local") {
console.log("本地文件" + props.fileType);
isImage(props.fileType) && localImagePreview(props.raw);
isDoc(props.fileType) && localOfficePreview(props.raw);
isText(props.fileType) && localTextPreview(props.raw);
isVideo(props.fileType) && localVideoPreview(props.raw);
isAudio(props.fileType) && localAudioPreview(props.raw);
} else {
if (isVideo(props.fileType)) {
src.value =
import.meta.env.VITE_APP_BASE_URL +
"/upload/attachments/getTeamOfVideo?id=" +
props.id;
return;
}
if (isText(props.fileType)) {
const response = await axios.get(
import.meta.env.VITE_STATIC_URL + props.filePath,
{
responseType: "text"
}
);
src.value = response.data;
return;
}
src.value = import.meta.env.VITE_STATIC_URL + props.filePath;
}
}
/**
*
* @param type
*/
function isImage(type: string) {
const types = [
"jpg",
"png",
"gif",
"jpeg",
"bmp",
"webp",
"svg",
"tiff",
"tif",
"jpeg",
"jfif",
"pjpeg",
"pjp"
];
return types.includes(type);
}
/**
*
* @param type
*/
function isDoc(type: string) {
const types = ["docx", "doc", "xlsx", "xls", "pdf"];
return types.includes(type);
}
/**
*
* @param type
*/
function isText(type: string) {
const types = [
"txt",
"md",
"log",
"json",
"xml",
"html",
"css",
"js",
"java",
"c",
"cpp",
"h",
"hpp",
"py",
"rb",
"go",
"sh",
"bat",
"ps1",
"psm1",
"ps1xml",
"psc1",
"psd1",
"psm1",
"ps1xml",
"psc1",
"psd1",
"ps1xml",
"psc1",
"ps1xml",
"psc1",
"psd1"
];
return types.includes(type);
}
// 检验视频格式
function isVideo(type: string) {
const types = [
"mp4",
"avi",
"rmvb",
"mkv",
"flv",
"wmv",
"mov",
"webm",
"m4v",
"mpg",
"mpeg",
"3gp",
"3g2",
"vob",
"ogv",
"ogg",
"mts",
"m2ts",
"ts",
"m2v",
"mpe",
"mpv",
"m4p",
"m4v",
"mpv2",
"m4v",
"m4p",
"m4v",
"m4p"
];
return types.includes(type);
}
// 检验音频文件
function isAudio(type: string) {
const types = ["mp3", "wav", "ogg", "flac", "aac", "wma", "m4a", "wma"];
return types.includes(type);
}
/**
* Office文件
* @param file
*/
function localOfficePreview(file: File) {
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = loadEvent => {
const arrayBuffer = loadEvent.target.result;
const blob = new Blob([arrayBuffer], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
});
src.value = URL.createObjectURL(blob);
};
}
/**
*
* @param file
*/
function localImagePreview(file: File) {
const reader = new FileReader();
reader.onload = e => {
src.value = e.target.result;
};
reader.readAsDataURL(file);
}
/**
*
* @param file
*/
function localTextPreview(file: File) {
const reader = new FileReader();
reader.onload = e => {
src.value = e.target.result;
};
reader.readAsText(file);
}
/**
*
* @param file
*/
function localVideoPreview(file: File) {
src.value = URL.createObjectURL(file);
}
function localAudioPreview(file: File) {
const reader = new FileReader();
reader.onloadend = () => {
const blob = new Blob([reader.result], { type: "audio/mpeg" });
src.value = URL.createObjectURL(blob);
};
reader.readAsArrayBuffer(file);
}
// 清理工作当组件卸载时释放URL对象
onUnmounted(() => {
if (src.value) {
isVideo(props.fileType) && URL.revokeObjectURL(src.value);
isAudio(props.fileType) && URL.revokeObjectURL(src.value);
}
});
return {
excelOptions,
src,
filePreviewRef,
isImage,
renderedHandler,
errorHandler,
renderTheFile,
isVideo,
isText,
isAudio
};
}

View File

@ -92,7 +92,9 @@ const getData = () => {
// //
homeworklist({ homeworklist({
classidarray: classListIds.value.join(','), classidarray: classListIds.value.join(','),
entpcourseid: '', // id //entpcourseid: '', // id
edustage: userStore.edustage,//
edusubject: userStore.edusubject,//
orderby: 'uniquekey DESC', orderby: 'uniquekey DESC',
pageSize: 100 pageSize: 100
}).then((response) => { }).then((response) => {
@ -101,7 +103,7 @@ const getData = () => {
response.rows[i].workdatalist = [] response.rows[i].workdatalist = []
response.rows[i].workdatacount = 0 // response.rows[i].workdatacount = 0 //
response.rows[i].workdatalistVisible = false response.rows[i].workdatalistVisible = false
response.rows[i].workdatafeedbackcount = 0 response.rows[i].workdatafeedbackcount = 0 //
response.rows[i].feedtimelength = 0 response.rows[i].feedtimelength = 0
response.rows[i].rightAnswerCount = 0 response.rows[i].rightAnswerCount = 0
response.rows[i].scoingRate = 0 + '%' // response.rows[i].scoingRate = 0 + '%' //
@ -142,10 +144,10 @@ const getData = () => {
} }
} }
// >0 // (workdatacount)>0
if (response.rows && response.rows.length > 0) { if (response.rows && response.rows.length > 0) {
classWorkList.value = classWorkList.value = response.rows && response.rows.filter((item) => item.workdatacount > 0)
response.rows && response.rows.filter((item) => item.workdatacount > 0) // classWorkList.value = response.rows && response.rows.filter((item) => item.workdatacount > 0 && item.uniquekey == '-0808-1')
//TODO total //TODO total
total.value = response.total total.value = response.total
} }
@ -211,7 +213,9 @@ const getStudentVisible = async () => {
// //
const response = await homeworklist({ const response = await homeworklist({
classidarray: classListIds.value.join(','), classidarray: classListIds.value.join(','),
entpcourseid: '', // //entpcourseid: '', // id
edustage: userStore.edustage,//
edusubject: userStore.edusubject,//
orderby: 'uniquekey DESC', orderby: 'uniquekey DESC',
pageSize: 100 pageSize: 100
}) })
@ -236,12 +240,8 @@ const getStudentVisible = async () => {
(classWorkList.value[t].workdataresultcount / classWorkList.value[t].workdatacount) * 100 (classWorkList.value[t].workdataresultcount / classWorkList.value[t].workdatacount) * 100
) )
// //
console.log('平均用时??---', classWorkList.value[t].workdatafeedbackcount)
if (classWorkList.value[t].workdatafeedbackcount > 0) { if (classWorkList.value[t].workdatafeedbackcount > 0) {
classWorkList.value[t].averagetime = ( classWorkList.value[t].averagetime = (classWorkList.value[t].feedtimelength / classWorkList.value[t].workdatafeedbackcount).toFixed(0)
classWorkList.value[t].feedtimelength / classWorkList.value[t].workdatafeedbackcount
).toFixed(0)
console.log('平均用时---', classWorkList.value[t].averagetime)
} else { } else {
classWorkList.value[t].averagetime = 0 classWorkList.value[t].averagetime = 0
} }
@ -257,15 +257,18 @@ const getStudentVisible = async () => {
const getStudentClassWorkData = () => { const getStudentClassWorkData = () => {
// //
console.log('======????????""""""""""') console.log('======????????""""""""""')
//TODO id
listClassworkdata({ listClassworkdata({
classids: classListIds.value.join(','), classids: classListIds.value.join(','),
entpcourseid: '', // id //entpcourseid: '', // id
edustage: userStore.edustage,//
edusubject: userStore.edusubject,//
orderby: "deaddate DESC",
pageSize: 1000 pageSize: 1000
}).then((res) => { }).then((res) => {
console.log('多个班级里,每个学生的作业数据', res.rows)
for (var t = 0; t < classWorkList.value.length; t++) { for (var t = 0; t < classWorkList.value.length; t++) {
for (var i = 0; i < res.rows.length; i++) { for (var i = 0; i < res.rows.length; i++) {
if (res.rows[i].classworkid == classWorkList.value[t].id) { if (res.rows[i].uniquekey == classWorkList.value[t].uniquekey) {
// if (res.rows[i].classworkid == classWorkList.value[t].id && res.rows[i].resultcount > 0) { // if (res.rows[i].classworkid == classWorkList.value[t].id && res.rows[i].resultcount > 0) {
console.log('==================') console.log('==================')
// / // /
@ -332,10 +335,7 @@ const getStudentClassWorkData = () => {
// //
if (classWorkList.value[t].workdatafeedbackcount > 0) { if (classWorkList.value[t].workdatafeedbackcount > 0) {
classWorkList.value[t].averagetime = classWorkList.value[t].averagetime = (classWorkList.value[t].feedtimelength / classWorkList.value[t].workdatafeedbackcount).toFixed(0)
(
classWorkList.value[t].feedtimelength / classWorkList.value[t].workdatafeedbackcount
).toFixed(0)
} else { } else {
classWorkList.value[t].averagetime = 0 classWorkList.value[t].averagetime = 0
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<el-form ref="classWorkFormScoreRef" :model="classWorkFormScore"> <el-form ref="classWorkFormScoreRef" :model="classWorkFormScore">
<div class="teacher_content" :style="{ height: dialogProps.maxheight + 'px' }"> <!-- <div class="teacher_content" :style="{ height: dialogProps.maxheight + 'px' }"> -->
<div class="teacher_content" :style="{ height: '75vh' }">
<div style="font-size: 18px; width: 100%; padding: 5px 10px" class="sticky"> <div style="font-size: 18px; width: 100%; padding: 5px 10px" class="sticky">
{{ classWorkFormScore.name }} 答题详情 {{ classWorkFormScore.name }} 答题详情
</div> </div>
@ -410,11 +411,12 @@
</div> </div>
</div> </div>
<!-- :style="{ height: dialogProps.maxheight + 'px' }" -->
<el-dialog <el-dialog
v-model="fileReadopen" v-model="fileReadopen"
title="文件预览" title="文件预览"
width="80%" width="80%"
:style="{ height: dialogProps.maxheight + 'px' }" :style="{ height: '75vh' }"
append-to-body append-to-body
> >
<div class="file-read-dialog"> <div class="file-read-dialog">
@ -468,13 +470,13 @@
></span ></span
> >
</div> </div>
<!-- <ReFilePreview <ReFilePreview
:name="fileitem.name" :name="fileitem.name"
:type="fileitem.type" :type="fileitem.type"
:file-type="fileitem.type" :file-type="fileitem.type"
:file-path="fileitem.url" :file-path="fileitem.url"
:text-content="textContent" :text-content="textContent"
/> --> />
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@ -488,12 +490,10 @@ import useUserStore from '@/store/modules/user'
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
// import { Plus } from '@element-plus/icons-vue' // import { Plus } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { updateClassworkeval } from '@/api/classTask' import { updateClassworkeval, updateClassworkdata } from '@/api/classTask'
import { getTimeDate } from '@/utils/date' import { getTimeDate } from '@/utils/date'
// import ReFilePreview from '@/components/ReFilePreview/index.vue' import ReFilePreview from '@/components/refile-preview/index.vue'
// import mammoth from 'mammoth'
// import * as XLSX from 'xlsx'
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter() const router = useRouter()
@ -864,6 +864,16 @@ const onSubmit = () => {
return return
} }
var formd = {
id: dialogProps.value.studentObj.id, // this.activeClassWork.id;
status: '1',//0 1
updatedate: getTimeDate,// = year+'-'+month+'-'+day+' '+hh+':'+mm;
};
//
updateClassworkdata(formd).then(res => {
})
//
classWorkFormScore.teacherRating && classWorkFormScore.teacherRating &&
classWorkFormScore.teacherRating.map((item, index) => { classWorkFormScore.teacherRating.map((item, index) => {
const queryParams = { const queryParams = {

View File

@ -54,12 +54,11 @@
</div> </div>
</template> </template>
<!-- 如果当前学习没有试题 --> <!-- 如果当前学习没有试题 :height="mainHeight"-->
<div <div
v-if="classWorkAnalysis.view == 'studentview'" v-if="classWorkAnalysis.view == 'studentview'"
style="width: 100%" style="width: 100%; height:75vh; "
class="clwk_dialog_view" class="clwk_dialog_view"
:height="mainHeight"
> >
<div class="view_table"> <div class="view_table">
<el-radio-group <el-radio-group
@ -75,6 +74,7 @@
v-loading="loading_dt_table" v-loading="loading_dt_table"
:data="tableRadio.list" :data="tableRadio.list"
row-key="id" row-key="id"
style="height: 69vh;"
highlight-current-row highlight-current-row
@row-click="getStudentClassWorkDataDetail" @row-click="getStudentClassWorkDataDetail"
> >
@ -491,6 +491,25 @@ const handleClassWorkAnalysissScoreOpen = (row) => {
} }
//#endregion //#endregion
/** 批阅:已交未交事件 */
const tableRadioChange = (e) => {
// ui
isopen_dtwk_table.value = false;
console.log(e,'??????')
console.log("学生列表:", classWorkAnalysis.classworkdata)
if(e=='1'){
tableRadio.list = classWorkAnalysis.classworkdata.filter(item => item.resultcount > 0)
tableRadio.value = '1';
tableRadio.num0 = classWorkAnalysis.classworkdata.length - tableRadio.list.length;
tableRadio.num1 = tableRadio.list.length;
}else if(e=='0'){
tableRadio.list = classWorkAnalysis.classworkdata.filter(item => item.resultcount == 0)
tableRadio.value = '0';
tableRadio.num0 = tableRadio.list.length;
tableRadio.num1 = classWorkAnalysis.classworkdata.length - tableRadio.list.length;
}
}
// //
const escapeHtmlQuotes = (str) => { const escapeHtmlQuotes = (str) => {
// replace, // replace,

View File

@ -14,13 +14,16 @@
&nbsp;|&nbsp; 截止时间{{ item.deaddate }} &nbsp;|&nbsp;{{ tabactive }} &nbsp;|&nbsp; 截止时间{{ item.deaddate }} &nbsp;|&nbsp;{{ tabactive }}
</div> </div>
</div> </div>
<el-switch v-model="value1" active-text="云同步"> </el-switch> <!-- <el-switch v-model="value1" active-text="云同步"> </el-switch> -->
<div class="class-reserv-item-tool"> <div class="class-reserv-item-tool">
<span><span style="color: #000fff; font-weight: 900; font-size: 15px">{{ item.workdataresultcount }}</span>/{{ item.workdatacount }}</span> <span>
<span v-if="item.workdataresultcount!=0" style="color:#000fff; font-weight: 900; font-size: 15px">{{ item.workdataresultcount }}</span>
<span v-if="item.workdataresultcount==0">{{ item.workdataresultcount }}</span>
/{{ item.workdatacount }}</span>
<span>已交</span> <span>已交</span>
</div> </div>
<div class="class-reserv-item-tool"> <div class="class-reserv-item-tool">
<span style="color: #ff7f00; font-weight: 900; font-size: 15px">2</span> <span style="color: #ff7f00; font-weight: 900; font-size: 15px">{{ item.teacherRationgCount?item.teacherRationgCount:0 }}</span>
<span>待批阅</span> <span>待批阅</span>
</div> </div>
<div class="class-reserv-item-tool"> <div class="class-reserv-item-tool">