ClassRecord.vue 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992
  1. <template>
  2. <div class="class-record-container">
  3. <!--头部信息-->
  4. <div class="class-record-header">
  5. <span class="back-page" @click="goBack">
  6. <Icon type="md-arrow-round-back" />
  7. {{$t('cusMgt.rcd.rtn')}}
  8. </span>
  9. <span class="course-name">
  10. {{recordInfo.name}}
  11. </span>
  12. <div style="display:inline-block;margin-left:30px">
  13. <span class="label-text">
  14. {{$t('cusMgt.rcd.ctime')}}
  15. </span>
  16. <span class="label-value">
  17. {{$jsFn.timeFormat(recordInfo.startTime)}}
  18. </span>
  19. </div>
  20. <div class="action-wrap">
  21. <!-- 下载学生作品 -->
  22. <span v-show="taskData.length" class="e-note-tag" @click="downloadStuWork">
  23. <Icon type="md-download" />
  24. {{$t('cusMgt.rcd.dlStuWrk')}}
  25. </span>
  26. <!-- 电子笔记 -->
  27. <span class="e-note-tag" @click="viewENote">
  28. <Icon type="ios-paper" />
  29. {{$t('cusMgt.rcd.enote')}}
  30. </span>
  31. <!-- 表格下载 -->
  32. <span class="e-note-tag" @click="downloadData">
  33. <Icon type="md-download" />
  34. {{$t('cusMgt.rcd.exportData')}}
  35. </span>
  36. <!-- 数据统计 -->
  37. <span class="e-note-tag" v-show="hasVideo" @click="isShowVd = !isShowVd">
  38. <Icon :type="isShowVd ? 'md-podium' : 'logo-youtube'" />
  39. {{isShowVd ? $t('cusMgt.rcd.dataCount') : $t('cusMgt.rcd.videoData')}}
  40. </span>
  41. </div>
  42. </div>
  43. <Split v-model="split1">
  44. <div slot="left" class="rcd-split-pane">
  45. <!--上课内容-->
  46. <vuescroll>
  47. <!-- <div :class="isShowBar ? 'class-content mouse-over-status':'class-content mouse-over-status'" @mousemove="isShowBar = true" @mouseleave="isShowBar = false"> -->
  48. <div class="class-content mouse-over-status">
  49. <video-player2 v-if="hasVideo && playerOptions.sources.length" v-show="isShowVd" @on-vd-error="videoError" class="video-player-box" :markers="markers" ref="videoPlayer" :options="playerOptions" :playsinline="true" @getCurPage="getCurPage">
  50. </video-player2>
  51. <div v-show="!isShowVd" class="video-player-box" style="padding:15px 0px" v-if="!isGlobalSite">
  52. <!-- <Alert v-show="!hasVideo" class="no-video-tips" type="warning" show-icon>
  53. {{$t('cusMgt.rcd.noVideo')}}
  54. </Alert> -->
  55. <!-- 大陆站显示四大维度数据 -->
  56. <div style="width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center" v-show="!isShowBase">
  57. <div style="display: flex;justify-content: center;font-weight: bold">
  58. <span style="display: flex;justify-content: center;flex-direction: column;align-items: center">
  59. <span :style="{ backgroundColor: getLevelColor(recordInfo.tLevel),boxShadow:'0 0 6px ' + getLevelColor(recordInfo.tLevel) }" class="level-light"></span>
  60. <span>{{ this.$t('lessonRecord.mgt.tLevel') }}</span>
  61. </span>
  62. <span style="display: flex;justify-content: center;flex-direction: column;align-items: center;margin-left: 40px;">
  63. <span :style="{ backgroundColor: getLevelColor(recordInfo.pLevel),boxShadow:'0 0 6px ' + getLevelColor(recordInfo.pLevel) }" class="level-light"></span>
  64. <span>{{ this.$t('lessonRecord.mgt.pLevel') }}</span>
  65. </span>
  66. </div>
  67. <div class="static-box">
  68. <div class="static-item">
  69. <span class="static-value">{{ recordInfo.clientInteractionAverge }}</span>
  70. <span class="static-label">{{ this.$t('lessonRecord.mgt.interaction') }}</span>
  71. </div>
  72. <div class="static-item">
  73. <span class="static-value">{{ recordInfo.hitaClientCmpCount }}</span>
  74. <span class="static-label">{{ this.$t('lessonRecord.mgt.cooperation') }}</span>
  75. </div>
  76. <div class="static-item">
  77. <span class="static-value">{{ recordInfo.examCount }}</span>
  78. <span class="static-label">{{ this.$t('lessonRecord.mgt.exam') }}</span>
  79. </div>
  80. <div class="static-item">
  81. <span class="static-value">{{ recordInfo.learningCategory.task }}</span>
  82. <span class="static-label">{{ this.$t('lessonRecord.mgt.task') }}</span>
  83. </div>
  84. <div class="static-item">
  85. <span class="static-value">{{ recordInfo.learningCategory.diffential }}</span>
  86. <span class="static-label">{{ this.$t('lessonRecord.mgt.diffential') }}</span>
  87. </div>
  88. </div>
  89. <BaseReportRadar v-if="recordInfo" :lessonInfo="recordInfo"></BaseReportRadar>
  90. </div>
  91. <DataCount :rcdInfo="baseData.report" v-if="baseData" v-show="isShowBase"></DataCount>
  92. <Icon type="md-swap" class="action-icon" color="#25b0c4" size="20" v-if="baseData" @click="isShowBase = !isShowBase" :title="isShowBase ? $t('lessonRecord.viewReport') : $t('lessonRecord.viewSummary')" />
  93. <!-- <Icon type="md-pie" class="action-icon" color="#25b0c4" size="20" @click="reportModal = true" :title="$t('lessonRecord.action1')" /> -->
  94. </div>
  95. <!-- 国际站显示统计数据 -->
  96. <DataCount :rcdInfo="baseData.report" v-else-if="baseData"></DataCount>
  97. <div class="courseware-wrap">
  98. <div class="custom-page-change custom-prev" @click="changePage('prev')">
  99. <Icon type="ios-arrow-back" />
  100. </div>
  101. <div class="custom-page-change custom-next" @click="changePage('next')">
  102. <Icon type="ios-arrow-forward" />
  103. </div>
  104. <img :src="curImg" alt="" class="course-cur-img">
  105. <div class="cus-page-wrap">
  106. <Page :total="pageList.length" :current.sync="curPage" :page-size="1" size="small" @on-change="getCurHTEX" />
  107. </div>
  108. </div>
  109. </div>
  110. </vuescroll>
  111. </div>
  112. <div slot="right" class="rcd-split-pane">
  113. <!--课堂互动记录-->
  114. <div class="top-wrap">
  115. <h2 class="content-title">
  116. {{$t('cusMgt.rcd.rcdLabel')}}
  117. </h2>
  118. <div style="margin-top:5px;margin-right:3px">
  119. <RadioGroup v-model="filterType" size="small" @on-change="handleFilter">
  120. <Radio v-for="item in filterInte" :key="item.value" :label="item.value" border>
  121. {{item.text}}
  122. <!-- {{item.value == 'all' ? '' : `(${item.count})`}} -->
  123. </Radio>
  124. </RadioGroup>
  125. </div>
  126. </div>
  127. <vuescroll ref="pagewrap">
  128. <div class="cus-data-wrap">
  129. <div class="interaction-record-wrap">
  130. <template v-for="(item,index) in pageListShow">
  131. <div v-if="item.pageData && item.pageData.length" class="page-item" :key="index" :id="'page'+(item.page)">
  132. <div class="page-info-wrap">
  133. <span class="page-tag" @click="toVideo(index+1,$event)" :ref="'page'+(index+1)">
  134. {{`${$t('cusMgt.rcd.cw')}${item.page}${$t('cusMgt.rcd.page')}`}}
  135. </span>
  136. <!-- 课件缩略图 -->
  137. <img class="page-mini-img" @click="openViewer(item.img)" :src="item.img" />
  138. <p class="class-time">
  139. <!-- <Icon size="16" type="md-time" /> -->
  140. <span class="class-time-value">{{item.time}}</span>
  141. </p>
  142. <!-- <Timeline style="margin-top:10px;margin-left:-5px">
  143. <TimelineItem v-for="(rtItem, rtIndex) in item.pageData" :key="rtIndex +''+index">
  144. <Icon type="md-arrow-dropright" slot="dot" />
  145. <p class="event-tag" @click="toEvent(rtItem)">{{rtItem.eventName}}</p>
  146. </TimelineItem>
  147. </Timeline> -->
  148. </div>
  149. <div class="record-data-wrap">
  150. <!-- 互动数据 -->
  151. <div v-for="event in item.pageData" :key="event.Time">
  152. <!-- 即问即答 -->
  153. <PopQues class="event-item" v-if="event.Event === 'PopQuesLoad' || event.Event === 'ReAtmpAnsStrt'" :evtType="event.Event" :irsData="event.data" :students="baseData.student" :recordInfo="recordInfo" :blobInfo="blobInfo"></PopQues>
  154. <!-- 抢权 -->
  155. <Buzr class="event-item student-event" v-else-if="event.Event === 'BuzrAns'" :buzrData="event.data" :students="baseData.student"></Buzr>
  156. <!-- 推送 -->
  157. <Push class="event-item" v-else-if="event.Event === 'FastPgPush'" :pushData="event.data"></Push>
  158. <!-- 作品收集 -->
  159. <Receive :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkSpaceLoad'" :rcvData="event.data" :students="baseData.student" :blobInfo="blobInfo"></Receive>
  160. <!-- 作品回帖 -->
  161. <WrkCmp :recordInfo="recordInfo" class="student-event event-item" v-else-if="event.Event === 'WrkCmp'" :cmpData="event.data" :students="baseData.student" :blobInfo="blobInfo"></WrkCmp>
  162. <!-- 随机挑人 -->
  163. <Pick class="event-item student-event" :pickData="event.data" v-else-if="event.Event === 'PickupResult'" :students="baseData.student"></Pick>
  164. <!-- 课中评测 -->
  165. <Exam class="event-item" :examInfo="event.data" :recordInfo="recordInfo" v-else-if="event.Event === 'SPQStrt'"></Exam>
  166. <!-- 智慧评分 -->
  167. <SmartRating class="event-item student-event" :recordInfo="recordInfo" :smartRate="event.data" :students="baseData.student" :vote="event.vote" :blobInfo="blobInfo" v-else-if="event.Event === 'RatingStart'"></SmartRating>
  168. <!-- 协作 -->
  169. <Cowork class="event-item student-event" :recordInfo="recordInfo" :blobInfo="blobInfo" :cowork="event.data" :students="baseData.student" v-else-if="event.Event === 'CoworkLoad'"></Cowork>
  170. </div>
  171. </div>
  172. </div>
  173. </template>
  174. </div>
  175. </div>
  176. </vuescroll>
  177. </div>
  178. </Split>
  179. <div v-if="openImageViewer" class="image-viewer" @click="closeViewer()">
  180. <Icon type="md-close" class="close-icon" @click="closeViewer()" />
  181. <img :src="viewUrl" class="animated fadeIn" @click.stop />
  182. </div>
  183. <!--返回顶部-->
  184. <BackToTop @on-to-top="handleToTop"></BackToTop>
  185. <!-- 课堂报告弹窗 -->
  186. <!-- <Modal v-model="reportModal" transfer width="860" footer-hide class="record-research-modal">
  187. <div slot="header">
  188. <p style="text-align: center;font-weight: bold">【{{ recordInfo.name }}】 智慧课堂报告</p>
  189. </div>
  190. <div>
  191. <div style="display: flex;justify-content: center;font-weight: bold">
  192. <span style="display: flex;justify-content: center;flex-direction: column;align-items: center">
  193. <span :style="{ backgroundColor: getLevelColor(recordInfo.tLevel),boxShadow:'0 0 6px ' + getLevelColor(recordInfo.tLevel) }" class="level-light"></span>
  194. <span>技术应用</span>
  195. </span>
  196. <span style="display: flex;justify-content: center;flex-direction: column;align-items: center;margin-left: 40px;">
  197. <span :style="{ backgroundColor: getLevelColor(recordInfo.pLevel),boxShadow:'0 0 6px ' + getLevelColor(recordInfo.pLevel) }" class="level-light"></span>
  198. <span>教法应用</span>
  199. </span>
  200. </div>
  201. <div class="static-box">
  202. <div class="static-item">
  203. <span class="static-value">{{ recordInfo.attendCount }}</span>
  204. <span class="static-label">出席人数</span>
  205. </div>
  206. <div class="split-line"></div>
  207. <div class="static-item">
  208. <span class="static-value">{{ recordInfo.groupCount }}</span>
  209. <span class="static-label">小组数</span>
  210. </div>
  211. <div class="split-line"></div>
  212. <div class="static-item">
  213. <span class="static-value">{{ recordInfo.collateTaskCount }}</span>
  214. <span class="static-label">任务总数</span>
  215. </div>
  216. <div class="split-line"></div>
  217. <div class="static-item">
  218. <span class="static-value">{{ recordInfo.collateCount }}</span>
  219. <span class="static-label">作品总数</span>
  220. </div>
  221. <div class="split-line"></div>
  222. <div class="static-item">
  223. <span class="static-value">{{ recordInfo.pushCount }}</span>
  224. <span class="static-label">推送资源数</span>
  225. </div>
  226. <div class="static-item">
  227. <span class="static-value">{{ recordInfo.totalPoint }}</span>
  228. <span class="static-label">总计分</span>
  229. </div>
  230. <div class="split-line"></div>
  231. <div class="static-item">
  232. <span class="static-value">{{ recordInfo.examCount }}</span>
  233. <span class="static-label">测验总数</span>
  234. </div>
  235. <div class="split-line"></div>
  236. <div class="static-item">
  237. <span class="static-value">{{ recordInfo.interactionCount }}</span>
  238. <span class="static-label">互动题数</span>
  239. </div>
  240. <div class="split-line"></div>
  241. <div class="static-item">
  242. <span class="static-value">{{ recordInfo.examPointRate }}%</span>
  243. <span class="static-label">测验得分率</span>
  244. </div>
  245. <div class="split-line"></div>
  246. <div class="static-item">
  247. <span class="static-value">{{ recordInfo.clientInteractionCount }}</span>
  248. <span class="static-label">学生互动总数</span>
  249. </div>
  250. </div>
  251. <BaseReportPie v-if="reportModal" :lessonInfo="recordInfo"></BaseReportPie>
  252. </div>
  253. </Modal> -->
  254. </div>
  255. </template>
  256. <script>
  257. import BaseReportPie from '@/view/research-center/BaseReportPie.vue'
  258. import BaseReportRadar from '@/view/research-center/BaseReportRadar.vue'
  259. import WrkCmp from './eventchart/WrkCmp.vue'
  260. import PopQues from './eventchart/PopQues.vue'
  261. import Buzr from './eventchart/Buzr.vue'
  262. import Pick from './eventchart/Pick.vue'
  263. import Push from './eventchart/Push.vue'
  264. import Exam from './eventchart/Exam.vue'
  265. import Receive from './eventchart/Receive.vue'
  266. import DataCount from './eventchart/DataCount.vue'
  267. import SmartRating from './eventchart/SmartRating.vue';
  268. import Cowork from './eventchart/Cowork.vue';
  269. import CountTo from 'vue-count-to'
  270. import BlobTool from '@/utils/blobTool.js'
  271. import FileSaver from "file-saver";
  272. import JSZip from "jszip";
  273. export default {
  274. components: {
  275. PopQues, Pick, Push, Receive, DataCount, CountTo, Buzr, Exam, WrkCmp, BaseReportPie, BaseReportRadar, SmartRating, Cowork
  276. },
  277. data() {
  278. return {
  279. filterType: 'all',
  280. reportModal: false,
  281. filterInte: [
  282. {
  283. value: 'all',
  284. text: this.$t('cusMgt.rcd.filter1'),
  285. },
  286. {
  287. value: 'Push',
  288. text: this.$t('cusMgt.rcd.filter2'),
  289. events: ['FastPgPush'],
  290. count: 0
  291. },
  292. {
  293. value: 'Task',
  294. text: this.$t('cusMgt.rcd.filter3'),
  295. events: ['WrkSpaceLoad', 'WrkCmp'],
  296. count: 0
  297. },
  298. {
  299. value: 'Interactive',
  300. text: this.$t('cusMgt.rcd.filter4'),
  301. events: ['PopQuesLoad', 'ReAtmpAnsStrt', 'BuzrLoad', 'BuzrAns'],
  302. count: 0
  303. },
  304. {
  305. value: 'SPQStrt',
  306. text: this.$t('cusMgt.rcd.filter5'),
  307. events: ['SPQStrt'],
  308. count: 0
  309. },
  310. {
  311. value: 'SmartRating',
  312. text: this.$t('cusMgt.rcd.filter6'),
  313. events: ['RatingStart'],
  314. count: 0
  315. },
  316. {
  317. value: 'CoworkLoad',
  318. text: this.$t('cusMgt.rcd.filter7'),
  319. events: ['CoworkLoad'],
  320. count: 0
  321. },
  322. ],
  323. split1: 0.4,
  324. backPage: undefined,
  325. baseData: {}, //base.json
  326. pushData: [],//push.json
  327. irsData: [],//irs.json
  328. taskData: [],//task.json
  329. smartData: [],//smartRating.json
  330. coworkData: [],//Cowork.json
  331. fnEvents: [],//功能事件
  332. events: [],//事件ID
  333. hiTeachEvent: [],//需要解析的事件信息
  334. isShowVd: true,
  335. isShowBase: false,
  336. hasVideo: true,
  337. eventClick: false,
  338. sokratesRecords: [],
  339. pageIds: [],
  340. pageEvents: [],
  341. pageList: [],
  342. pageListShow: [],
  343. videoUrl: '',
  344. videoPoster: '',
  345. recordInfo: {},
  346. isShowBar: false,
  347. markers: [],
  348. curPage: 1,
  349. viewUrl: '',
  350. openImageViewer: false,
  351. typeIndex: 'all',
  352. playerOptions: {
  353. height: "450px",
  354. controlBar: {
  355. durationDisplay: true, // 总时间
  356. currentTimeDisplay: true,
  357. },
  358. html5: {
  359. nativeControlsForTouch: true,
  360. },
  361. inactivityTimeout: 1,
  362. nativeVideoTracks: false,
  363. playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
  364. autoplay: true, //如果true,浏览器准备好时开始回放。
  365. controls: true, //控制条
  366. preload: 'auto', //视频预加载
  367. muted: false, //默认情况下将会消除任何音频。
  368. loop: false, //导致视频一结束就重新开始。
  369. language: 'zh-CN',
  370. //aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
  371. //fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
  372. sources: [],
  373. poster: '',
  374. // notSupportedMessage: '此视频暂无法播放,请稍后再试' //允许覆盖Video.js无法播放媒体源时显示的默认信息。
  375. },
  376. blobInfo: undefined,
  377. }
  378. },
  379. methods: {
  380. // 自定义换页功能
  381. changePage(type) {
  382. if (type == 'prev') {
  383. if (this.curPage > 1) this.curPage--
  384. } else if (type == 'next') {
  385. if (this.curPage < this.pageList.length) this.curPage++
  386. }
  387. },
  388. // 下载学生作品
  389. async downloadStuWork() {
  390. // 需要优化原始目录结构
  391. // const containerClient = BlobTool.CreateBlobTool(this.recordInfo.scope)
  392. // containerClient.downloadFolder(`records/${this.recordInfo.id}/Clients`, {
  393. // fileName: this.$t('cusMgt.rcd.stuWrk'),
  394. // exclude: 'taskList_localstorage'
  395. // })
  396. const containerClient = BlobTool.CreateBlobTool(this.recordInfo.scope)
  397. const zip = new JSZip()
  398. for (const task of this.taskData) {
  399. if (task.clientWorks && task.clientWorks.length) {
  400. for (const student of task.clientWorks) {
  401. if (student.blobFiles && student.blobFiles.length) {
  402. for (const file of student.blobFiles) {
  403. let blobPath = `records/${this.recordInfo.id}${file}`
  404. let blobClient = containerClient.containerClient.getBlockBlobClient(blobPath)
  405. const dwRes = await blobClient.download()
  406. const blobRes = await dwRes.blobBody
  407. const filePath = `${task.jobName}/${student.seatID}${file.substring(file.lastIndexOf("/") + 1)}`
  408. zip.file(filePath, blobRes, {
  409. binary: true
  410. }) // 逐个添加文件
  411. }
  412. }
  413. }
  414. }
  415. }
  416. zip.generateAsync({
  417. type: "blob"
  418. }).then(content => {
  419. // 生成二进制流
  420. FileSaver.saveAs(content, this.$t('cusMgt.rcd.stuWrk') + ".zip"); // 利用file-saver保存文件
  421. }).catch(err => {
  422. console.log(err);
  423. })
  424. },
  425. //下载统计表格
  426. downloadData() {
  427. // let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
  428. let url = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/summary.xlsx${this.blobInfo.blob_sas}`
  429. const downloadRes = async () => {
  430. let response = await fetch(url); // 内容转变成blob地址
  431. let blob = await response.blob(); // 创建隐藏的可下载链接
  432. let objectUrl = window.URL.createObjectURL(blob);
  433. let a = document.createElement('a');
  434. a.href = objectUrl;
  435. a.download = this.recordInfo.name + '.xlsx';
  436. a.click()
  437. a.remove();
  438. }
  439. downloadRes();
  440. },
  441. videoError() {
  442. this.hasVideo = false
  443. this.isShowVd = false
  444. },
  445. //返回顶部
  446. handleToTop() {
  447. this.$refs['pagewrap'].scrollTo(
  448. {
  449. y: '0'
  450. },
  451. 300
  452. )
  453. // this.$refs['datawrap'].scrollTo(
  454. // {
  455. // y: '0'
  456. // },
  457. // 300
  458. // )
  459. },
  460. //跳转到对应事件
  461. toEvent(event) {
  462. console.log(event)
  463. this.eventClick = true
  464. this.curPage = event.pageIndex + 1
  465. this.$refs.videoPlayer.player.currentTime(event.Time)
  466. },
  467. /**
  468. * 处理TimeLine的异常
  469. * 页面重复的IRS作答
  470. */
  471. fixEventData() {
  472. this.pageEvents = this.pageEvents.filter((item, index) => {
  473. if (item.Event == 'PopQuesLoad') {
  474. let firstIndex = this.pageEvents.findIndex(i => i.Pgid == item.Pgid && item.Event == i.Event)
  475. return firstIndex == index
  476. } else {
  477. return true
  478. }
  479. })
  480. },
  481. //小于10 补0
  482. fullZero(n) {
  483. return n < 10 ? `0${n}` : `${n}`
  484. },
  485. formatTime(time) {
  486. let h = Math.floor(time / (60 * 60))
  487. time = time % (60 * 60)
  488. let m = Math.floor(time / 60)
  489. let s = Math.floor(time % 60)
  490. return `${this.fullZero(h)}:${this.fullZero(m)}:${this.fullZero(s)}`
  491. },
  492. //根据SokratesRecords.json处理page数据
  493. async getPageList() {
  494. this.pageList = []
  495. this.markers = []
  496. // let blobInfo = this.recordInfo.scope == 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
  497. // 这里需要兼容原来没有TimeLine.json的课例(优先度读timeLine.json,没有则读SokratesRecords.json)
  498. let url = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/TimeLine.json${this.blobInfo.blob_sas}`
  499. let hasTimeLine = true
  500. let dataErr = false
  501. try {
  502. let res = await this.$tools.getFile(url)
  503. this.sokratesRecords = JSON.parse(res)
  504. this.pageIds = this.sokratesRecords.PgIdList || []
  505. this.pageEvents = this.sokratesRecords.events || []
  506. this.fixEventData()
  507. } catch (e) {
  508. hasTimeLine = false
  509. }
  510. //读取 timeLine.json 失败,则读取 SokratesRecords.json
  511. if (!hasTimeLine) {
  512. url = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/Sokrates/SokratesRecords.json${this.blobInfo.blob_sas}`
  513. try {
  514. let res = await this.$tools.getFile(url)
  515. let resJson = JSON.parse(res)
  516. // 处理成timeLine数据格式
  517. let pageidEvent = resJson.find(item => item.Event == 'PgidList')
  518. this.pageIds = pageidEvent && pageidEvent.PgIdList ? pageidEvent.PgIdList : []
  519. this.pageEvents = resJson.filter(item => this.events.includes(item.Event))
  520. this.fixEventData()
  521. this.sokratesRecords = {
  522. events: this.pageEvents,
  523. PgIdList: this.pageIds
  524. }
  525. } catch (e) {
  526. //timeLine 和 SokratesRecords都没有找到
  527. dataErr = true
  528. }
  529. }
  530. // 数据异常
  531. if (dataErr) {
  532. this.$Message.error(this.$t('cusMgt.rcd.dataErr'))
  533. return
  534. }
  535. // 时间轴数据正常
  536. //获取Push.json、IRS.json、Task.json、Base.json数据 新增SmartRating.json(智慧评分) 新增Cowork.json(协作)
  537. try {
  538. let pushUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/Push.json${this.blobInfo.blob_sas}`
  539. this.pushData = JSON.parse(await this.$tools.getFile(pushUrl) || '[]')
  540. this.pushData.map(item => {
  541. // 暂无法根据 type 区分物件化推送和图片推送,先判断是否有 .jpg 后缀
  542. if(item.pageMeta.indexOf('.jpg') != -1) {
  543. item.pageUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}${item.pageMeta}${this.blobInfo.blob_sas}`
  544. } else {
  545. item.pageUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}${item.pageMeta}/bg_snapshot_00001.jpg${this.blobInfo.blob_sas}`
  546. }
  547. })
  548. } catch (e) {
  549. this.pushData = []
  550. }
  551. try {
  552. let irsUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/IRS.json${this.blobInfo.blob_sas}`
  553. this.irsData = JSON.parse(await this.$tools.getFile(irsUrl) || '[]')
  554. } catch (e) {
  555. this.irsData = []
  556. }
  557. try {
  558. let taskUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/Task.json${this.blobInfo.blob_sas}`
  559. this.taskData = JSON.parse(await this.$tools.getFile(taskUrl) || '[]')
  560. } catch (e) {
  561. this.taskData = []
  562. }
  563. try {
  564. let baseUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/base.json${this.blobInfo.blob_sas}`
  565. this.baseData = JSON.parse(await this.$tools.getFile(baseUrl) || '{}')
  566. } catch (e) {
  567. this.baseUrl = {}
  568. }
  569. try {
  570. let smartUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/SmartRating.json${this.blobInfo.blob_sas}`
  571. this.smartData = JSON.parse(await this.$tools.getFile(smartUrl) || '[]')
  572. } catch (e) {
  573. this.smartData = []
  574. }
  575. try {
  576. let coworkUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/IES/Cowork.json${this.blobInfo.blob_sas}`
  577. this.coworkData = JSON.parse(await this.$tools.getFile(coworkUrl) || '[]')
  578. } catch (e) {
  579. this.coworkData = []
  580. }
  581. let pgids = this.pageIds
  582. //这里需要判断录制开始的pageid
  583. // let startInfo = this.pageEvents?.findLast(item => item.Event === 'EzsStartRecord')
  584. let startInfo = this._.findLast(this.pageEvents, item => item.Event === 'EzsStartRecord') //排查兼容性问题
  585. let startId = startInfo ? startInfo.Pgid : ''
  586. let startIndex = 0
  587. if (startId) {
  588. startIndex = pgids.findIndex(item => item === startId)
  589. //第一页视频标记
  590. this.markers.push({
  591. time: startInfo.Time,
  592. text: `${this.$t('cusMgt.rcd.di')}${1}${this.$t('cusMgt.rcd.page')}`,
  593. page: 1
  594. })
  595. }
  596. if (startIndex > -1) {
  597. pgids = pgids.slice(startIndex)
  598. }
  599. pgids.forEach((item, index) => {
  600. let page = {}
  601. page.id = item
  602. page.img = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/Memo/${item}.jpg${this.blobInfo.blob_sas}`
  603. page.page = index + 1
  604. //当前页面对应的功能事件
  605. page.pageData = this.pageEvents.filter(record => record.Pgid === item && this.fnEvents.includes(record.Event))
  606. page.time = this.formatTime(page.pageData[0]?.Time)
  607. page.pageData.forEach(e => {
  608. e.pageIndex = index
  609. e.eventName = this.hiTeachEvent[e.Event]?.text
  610. let rlt = this.hiTeachEvent[e.Event]?.relation
  611. e.relation = rlt
  612. switch (rlt) {
  613. case 'irs':
  614. e.data = this.irsData.find(i => i.pageID == e.Pgid)
  615. this.filterInte[3].count++
  616. break
  617. case 'push':
  618. e.data = this.pushData.find(p => p.pageId == e.Pgid || p.pageID == e.Pgid) //数据兼容 pageId 有些大写有些小写
  619. this.filterInte[1].count++
  620. break
  621. case 'task':
  622. e.data = this.taskData.find(t => t.pageID == e.Pgid)
  623. this.filterInte[2].count++
  624. break
  625. case 'timeline':
  626. e.data = this._.cloneDeep(e)
  627. this.filterInte[3].count++
  628. break
  629. case 'exam':
  630. e.data = this._.cloneDeep(e)
  631. this.filterInte[4].count++
  632. break
  633. case 'smart':
  634. e.data = this.smartData.find(t => t.pageID == e.Pgid)
  635. e.data.smartRateSummary.rateInfo = {
  636. RatingSource: e.RatingSource,// 互评的作品类型 IRS:文字题 StudentWork:图片
  637. AnonyCandi: e.AnonyCandi,// 评论是否匿名
  638. }
  639. // 按照轮数返回的
  640. e.vote = undefined
  641. if(e.RatingType === 'Voting') {
  642. e.vote = {
  643. round: e.Round,
  644. votes: e.Votes
  645. }
  646. }
  647. this.filterInte[5].count++
  648. break
  649. case 'cowork':
  650. e.data = this.coworkData.find(t => t.pageID == e.Pgid)
  651. this.filterInte[6].count++
  652. break
  653. default:
  654. break
  655. }
  656. })
  657. for (let i = 1; i < page.pageData.length; i++) {
  658. // 星光大评分会操作几次就返回几条数据,所以需要去重只显示第一条
  659. if(page.pageData[i - 1].Event === 'RatingStart' && page.pageData[i].Event === 'RatingStart' && page.pageData[i].RatingType && page.pageData[i].RatingType === 'GrandRating' && page.pageData[i - 1].Pgid === page.pageData[i].Pgid) {
  660. page.pageData.splice(i, 1)
  661. i--
  662. }
  663. }
  664. this.pageList.push(page)
  665. })
  666. console.log(this.pageList)
  667. this.pageListShow = this.pageList
  668. console.log('互动数据', this.pageList)
  669. this.pageList.forEach((pageInfo, pageIndex) => {
  670. pageInfo.pageData.forEach(e => {
  671. if (e.Event == 'WrkCmp') {
  672. console.log(pageIndex, e)
  673. }
  674. })
  675. })
  676. let pageEvent = this.pageEvents.filter(item => item.Event === 'PgJump' || item.Event === 'PgAdd' || item.Event === 'DiscussStart')
  677. let page = this.markers.length
  678. pageEvent.forEach((item, index) => {
  679. if (item.JumpTo != item.JumpFrom) {
  680. this.markers.push({
  681. time: item.Time,
  682. text: `${this.$t('cusMgt.rcd.di')}${item.JumpTo}${this.$t('cusMgt.rcd.page')}`,
  683. page: item.JumpTo
  684. })
  685. }
  686. })
  687. console.log(this.markers)
  688. },
  689. //查看苏格拉底报告
  690. viewReport() {
  691. this.$router.push({
  692. name: 'teachCenter',
  693. query: { id: this.recordInfo.id, name: this.recordInfo.name }
  694. })
  695. },
  696. //查看电子笔记
  697. viewENote() {
  698. let eNote
  699. if (this.recordInfo.eNote) {
  700. eNote = this.recordInfo.eNote
  701. } else {
  702. let sasInfo = {}
  703. // let blobInfo = this.recordInfo.scope === 'school' ? this.$store.state.user.schoolProfile : this.$store.state.user.userProfile
  704. sasInfo.sas = this.blobInfo.blob_sas
  705. sasInfo.name = this.recordInfo.scope === 'school' ? this.$store.state.userInfo.schoolCode : this.$store.state.userInfo.TEAMModelId
  706. sasInfo.url = this.blobInfo.blob_uri.slice(0, this.blobInfo.blob_uri.lastIndexOf(sasInfo.name) - 1)
  707. eNote = `${sasInfo.url}/${sasInfo.name}/records/${this.recordInfo.id}/Note.pdf${sasInfo.sas}`
  708. }
  709. window.open('/web/viewer.html?file=' + encodeURIComponent(eNote))
  710. // if (this.recordInfo.eNote) {
  711. // } else {
  712. // this.$Message.warning(this.$t('cusMgt.rcd.noNote'))
  713. // }
  714. },
  715. //点击视频切片
  716. getCurPage(page) {
  717. // this.$refs["datawrap"].scrollIntoView('#page' + page, 500)
  718. this.$refs.videoPlayer.player.play()
  719. this.curPage = page
  720. },
  721. //点击课件page
  722. getCurHTEX(page) {
  723. if (this.eventClick) {
  724. this.eventClick = false
  725. return
  726. }
  727. this.$refs["pagewrap"].scrollIntoView('#page' + page, 500)
  728. // //视频时间定位
  729. // let pageInfo = this.markers.find(item => {
  730. // return item.page == page
  731. // })
  732. // if (pageInfo) {
  733. // this.$refs.videoPlayer.player.currentTime(pageInfo.time)
  734. // this.$refs.videoPlayer.player.play()
  735. // } else {
  736. // this.$refs["pagewrap"].scrollIntoView('#page' + page, 500)
  737. // }
  738. },
  739. //点击互动记录页面tag
  740. toVideo(page, e) {
  741. //页面滚动
  742. // let dataLoacation = this.$refs["datawrap"].getPosition()
  743. // let pageLocaltion = this.$refs["pagewrap"].getPosition()
  744. // let y = e.pageY - 665 + pageLocaltion.scrollTop + dataLoacation.scrollTop
  745. this.$nextTick(() => {
  746. // this.$refs["pagewrap"].scrollTo(
  747. // {
  748. // x: 0,
  749. // y: 0
  750. // }
  751. // )
  752. // this.$refs["datawrap"].scrollTo(
  753. // {
  754. // x: 0,
  755. // y: y
  756. // }
  757. // )
  758. })
  759. // 教材页面切换
  760. this.curPage = page
  761. //视频时间定位
  762. let pageInfo = this.markers.find(item => {
  763. return item.page == page
  764. })
  765. if (pageInfo) this.$refs.videoPlayer.player.currentTime(pageInfo.time)
  766. },
  767. //互动类型筛选
  768. handleFilter() {
  769. if (this.filterType == 'all') {
  770. this.pageListShow = this.pageList
  771. } else {
  772. let data = this._.cloneDeep(this.pageList)
  773. let filterInfo = this.filterInte.find(item => item.value === this.filterType)
  774. this.pageListShow = data.map(item => {
  775. if (item.pageData.length) {
  776. item.pageData = item.pageData.filter(event => {
  777. //if (this.filterType == 'Other') {
  778. // return !events.includes(event.Event)
  779. //} else {
  780. // return event.Event == this.filterType
  781. //}
  782. return filterInfo.events?.includes(event.Event)
  783. })
  784. }
  785. return item
  786. })
  787. }
  788. },
  789. //查看图片
  790. openViewer(url) {
  791. this.openImageViewer = true
  792. this.viewUrl = url
  793. },
  794. closeViewer() {
  795. this.openImageViewer = false
  796. },
  797. goBack() {
  798. if (this.backPage) {
  799. this.$router.push({
  800. name: this.backPage,
  801. record: this.recordInfo
  802. })
  803. } else {
  804. this.$router.go(-1)
  805. }
  806. },
  807. },
  808. computed: {
  809. isGlobalSite() {
  810. return this.$store.state.config.srvAdr !== 'China'
  811. },
  812. curImg() {
  813. console.log(this.curPage)
  814. if (this.pageList[this.curPage - 1]) {
  815. return this.pageList[this.curPage - 1].img
  816. } else {
  817. return ""
  818. }
  819. },
  820. getLevelColor() {
  821. return level => {
  822. if (level < 0) {
  823. return '#888999'
  824. } else {
  825. return ['#ff2d2d', '#ffc018', '#00b214'][level]
  826. }
  827. }
  828. }
  829. },
  830. async created() {
  831. this.hiTeachEvent = this.$GLOBAL.HI_TEACH_EVENT()
  832. //正式站先暂时不放出课中评测
  833. // if (this.$store.state.config.srvAdrType == 'product') {
  834. // this.$delete(this.hiTeachEvent, 'SPQStrt')
  835. // }
  836. this.events = Object.keys(this.hiTeachEvent)
  837. this.fnEvents = this.events.filter(key => this.hiTeachEvent[key].type === 'fn')
  838. console.log(this.events, this.fnEvents)
  839. this.recordInfo = this.$route.params.record || (sessionStorage.getItem('record') ? JSON.parse(sessionStorage.getItem('record')) : undefined)
  840. console.log(this.recordInfo)
  841. if (!this.recordInfo) {
  842. this.$router.go(-1)
  843. } else {
  844. sessionStorage.setItem('record', JSON.stringify(this.recordInfo))
  845. //对接Blob数据
  846. let blobInfos = this.recordInfo.scope == 'school' ? this.$store.state.user.schoolProfile : {...this.$store.state.user.userProfile}
  847. if(this.recordInfo.tmdid != this.$store.state.userInfo.TEAMModelId && this.recordInfo.scope != 'school') {
  848. // 其他老师创建的课堂记录
  849. let prvblobInfos = await this.$evTools.getBlobPrivateSasObj(this.recordInfo.tmdid)
  850. this.blobInfo = {
  851. blob_uri: prvblobInfos.url + '/' + prvblobInfos.name,
  852. blob_sas: prvblobInfos.sas
  853. }
  854. } else {
  855. this.blobInfo = {
  856. blob_uri: blobInfos.blob_uri,
  857. blob_sas: '?' + blobInfos.blob_sas
  858. }
  859. }
  860. console.error(this.blobInfo);
  861. this.videoUrl = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/Record/CourseRecord.mp4${this.blobInfo.blob_sas}`
  862. this.videoPoster = `${this.blobInfo.blob_uri}/records/${this.recordInfo.id}/Record/CoverImage.jpg${this.blobInfo.blob_sas}`
  863. this.playerOptions.poster = this.videoPoster
  864. this.playerOptions.sources.push({
  865. src: this.videoUrl,
  866. type: 'video/mp4'
  867. })
  868. this.getPageList()
  869. }
  870. },
  871. beforeRouteEnter(to, from, next) {
  872. next(vm => {
  873. if (from.name == 'homePage') {
  874. vm.backPage = 'course'
  875. }
  876. console.log(arguments)
  877. })
  878. },
  879. watch: {
  880. blobInfo: {
  881. handler(n, o) {
  882. console.log(n);
  883. },
  884. deep: true,
  885. }
  886. }
  887. }
  888. </script>
  889. <style lang="less" scoped>
  890. @import "./ClassRecord.less";
  891. </style>
  892. <style lang="less">
  893. .courseware-wrap {
  894. .ivu-page-next,
  895. .ivu-page-prev {
  896. display: none;
  897. }
  898. }
  899. .video-player-box {
  900. .level-light {
  901. display: inline-block;
  902. width: 25px;
  903. height: 25px;
  904. border-radius: 50px;
  905. margin: 10px 0;
  906. }
  907. .reportPie {
  908. height: 320px;
  909. }
  910. .static-box {
  911. display: flex;
  912. justify-content: center;
  913. background-color: #ffffff;
  914. width: 80%;
  915. margin: 20px 0;
  916. .split-line {
  917. width: 2px;
  918. height: 30px;
  919. margin: 0 20px;
  920. margin-top: 35px;
  921. background-color: rgb(238, 238, 238);
  922. }
  923. .static-item {
  924. position: relative;
  925. display: flex;
  926. flex-direction: column;
  927. align-items: center;
  928. justify-content: center;
  929. width: 45%;
  930. &:first-child {
  931. padding-left: 10px;
  932. }
  933. .static-value {
  934. font-size: 30px;
  935. font-weight: bold;
  936. color: #00b4eb;
  937. }
  938. }
  939. }
  940. }
  941. .top-wrap .ivu-radio-wrapper-checked {
  942. color: white;
  943. background: #2d8cf0;
  944. }
  945. .top-wrap .ivu-radio {
  946. display: none;
  947. }
  948. .class-content .vjs-progress-holder {
  949. font-size: 10px !important;
  950. }
  951. .mouse-over-status .vjs-control-bar {
  952. opacity: 1 !important;
  953. }
  954. .class-content .video-js .vjs-big-play-button {
  955. top: 50%;
  956. left: 50%;
  957. margin-left: -40px;
  958. margin-top: -20px;
  959. }
  960. .class-content .vjs_video_3-dimensions.vjs-fluid {
  961. padding-top: 0;
  962. }
  963. .class-content .video-js.vjs-fluid,
  964. .class-content .video-js.vjs-16-9,
  965. .class-content .video-js.vjs-4-3 {
  966. height: 450px;
  967. }
  968. .video-player-box .vjs-volume-panel {
  969. display: none;
  970. }
  971. .video-player-box .ivu-alert {
  972. border-radius: 0px;
  973. }
  974. </style>