index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. <template>
  2. <div class="pptEasyClass">
  3. <div class="pec_main" v-loading="pageLoading">
  4. <!-- 录音转文字 -->
  5. <iframe
  6. allow="camera *; microphone *;display-capture;midi;encrypted-media;"
  7. src="https://beta.cloud.cocorobo.cn/browser/public/index.html"
  8. ref="iiframe"
  9. v-show="false"
  10. ></iframe>
  11. <div class="pec_header">
  12. <div class="pec_h_left">
  13. <div @click.stop="back" class="backBtn" v-if="screenType != 2 || tType == 1">
  14. <img src="../../assets/icon/newIcon/return.svg" alt="" />
  15. </div>
  16. <div v-if="tcid" class="class-info-group">
  17. <span class="class-label">班级</span>
  18. <span class="class-value">{{ className }}</span>
  19. </div>
  20. <div v-if="tcid" class="class-info-group">
  21. <span class="class-label" v-if="inviteCode">识别码</span>
  22. <span class="class-value" v-if="inviteCode">{{ inviteCode }}</span>
  23. </div>
  24. </div>
  25. <div class="pec_h_center">
  26. <el-tooltip effect="dark" :content="courseDetail.title" placement="bottom">
  27. <div class="pec_h_l_title">
  28. <span>{{ courseDetail.title }}</span>
  29. </div>
  30. </el-tooltip>
  31. <div class="free-browse-switch" v-if="courseDetail.userid == userid">
  32. <el-switch
  33. v-model="freeBrowse"
  34. :active-value="false"
  35. :inactive-value="true"
  36. class="custom-switch"
  37. active-color="#03ae2b"
  38. inactive-color="#d8d8d8"
  39. @change="onFreeBrowseChange"
  40. ></el-switch>
  41. <span class="switch-label" :class="{ active: freeBrowse }">{{ freeBrowse ? '自由浏览' : '跟随模式' }}</span>
  42. </div>
  43. <div class="free-browse-switch" v-if="tType == 2">
  44. <span class="switch-label" :class="{ active: freeBrowse }">{{ freeBrowse ? '自由浏览' : '跟随模式' }}</span>
  45. </div>
  46. <el-tooltip effect="dark" content="刷新" placement="bottom">
  47. <div class="refresh_icon" @click="refreshCourse">
  48. <img src="../../assets/icon/course/refresh-2.svg" />
  49. </div>
  50. </el-tooltip>
  51. </div>
  52. <div class="pec_h_right">
  53. <div class="pec_h_r_btnArea">
  54. <div class="pec_h_r_btn_refresh" :class="{ 'recording': recordedForm.status == 1 }" @click="toggleRecording" v-show="(jArray.includes(oid) || jArray.includes(org)) && courseDetail.userid == userid">
  55. <span>{{ recordedForm.status == 1 ? '结束录音' : '开始录音' }}</span>
  56. </div>
  57. <div class="pec_h_r_btn_afterClass" @click="afterClass" v-if="courseDetail.userid == userid">
  58. <img src="../../assets/icon/newIcon/afterClass.svg" alt="" />
  59. <span>下课</span>
  60. </div>
  61. <div class="name_box" v-if="tType == 2">
  62. {{ userJson.username }}
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <div class="pec_content">
  68. <iframe allow="camera *; microphone *;display-capture;midi;encrypted-media;clipboard-write;clipboard-read"
  69. webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen="" frameborder="no" border="0" :src="iframeSrc"
  70. v-if="showIframe" style="width: 100%; height: 100%; border: none" ref="ppt"></iframe>
  71. </div>
  72. </div>
  73. </div>
  74. </template>
  75. <script>
  76. import { myMixin } from '../../mixins/mixin';
  77. export default {
  78. mixins: [myMixin],
  79. data() {
  80. return {
  81. id: this.$route.query.courseId,
  82. userid: this.$route.query.userid,
  83. classId: this.$route.query.cid,
  84. role: this.$route.query.role,
  85. oid: this.$route.query.oid,
  86. org: this.$route.query.org,
  87. tType: this.$route.query.tType,
  88. courseType: this.$route.query.type,
  89. screenType: this.$route.query.screenType,
  90. tcid2: this.$route.query.tcid,
  91. tcid: "",
  92. className: "",
  93. showIframe: false,
  94. iframeSrc: "",
  95. courseDetail: {},
  96. pageLoading: false,
  97. inviteCode: "",
  98. startTime: "",
  99. freeBrowse: true, // 默认自由浏览
  100. opertimer: null, // 定时器
  101. jArray: [],
  102. // 录音相关变量
  103. languageRadio: 2, // 语言选择
  104. recordedForm: {
  105. status: 0, // 0: 未开始, 1: 录音中, 2: 暂停, 3: 结束
  106. startTime: 0,
  107. endTime: 0,
  108. timeDuration: 0,
  109. textList: [],
  110. audioBlob: []
  111. },
  112. controlsStatus: 0, // 控制状态
  113. showIndexPage: true, // 显示索引页
  114. pageStatus: 1, // 页面状态
  115. editorBarData: {
  116. type: "0",
  117. content: ""
  118. },
  119. uploadFileLoading: false, // 上传文件加载状态
  120. transcriptionData: {
  121. content: ""
  122. },
  123. showGetTextLoading: false, // 显示获取文本加载状态
  124. };
  125. },
  126. methods: {
  127. goTo(path) {
  128. this.$router.push(path);
  129. },
  130. refreshCourse() {
  131. this.getCourseDetail();
  132. },
  133. audioStart(){
  134. this.onStartRecordWithMicrosoft();
  135. },
  136. toggleRecording() {
  137. if (this.recordedForm.status == 1) {
  138. this.onFinishRecordWithMicrosoft();
  139. } else {
  140. this.onStartRecordWithMicrosoft();
  141. }
  142. },
  143. // ============ start 微软录音转译
  144. onStartRecordWithMicrosoft() {
  145. let iiframe = this.$refs["iiframe"];
  146. iiframe.contentWindow.window.document.getElementById(
  147. "languageOptions"
  148. ).selectedIndex = this.languageRadio;
  149. // 录音开始
  150. let flag = true;
  151. console.log("开始录音", iiframe);
  152. this.recordedForm.status = 1;
  153. iiframe.contentWindow.window.onRecognizedResult = e => {
  154. console.log("onRecognizedResult", e);
  155. this.recordedForm.endTime = this.recordedForm.timeDuration;
  156. if (flag) {
  157. this.controlsStatus = 1;
  158. this.showIndexPage = false;
  159. this.pageStatus = 2;
  160. this.editorBarData.type = "0";
  161. flag = false;
  162. this.uploadFileLoading = false;
  163. this.transcriptionData.content = "";
  164. this.editorBarData.content = "";
  165. this.recordedForm.textList = [];
  166. }
  167. this.showGetTextLoading = true;
  168. let privText = e.privText;
  169. let privSpeakerId = e.privSpeakerId;
  170. let _copyPrivSpeakerId = privSpeakerId;
  171. console.log("👇转译对象👇");
  172. console.log(e);
  173. console.log("👇转译结果👇");
  174. console.log(privText);
  175. if (!privText || !privSpeakerId || privSpeakerId == "Unknown") {
  176. return;
  177. }
  178. const newItem = {
  179. value: privText,
  180. role: "",
  181. startTime: this.updateRecordedTime({
  182. duration: this.recordedForm.startTime
  183. }),
  184. endTime: this.updateRecordedTime({
  185. duration: this.recordedForm.endTime
  186. }),
  187. time: this.updateRecordedTime({
  188. duration: this.recordedForm.endTime - this.recordedForm.startTime
  189. })
  190. };
  191. this.recordedForm.textList.push(newItem);
  192. this.recordedForm.startTime = this.recordedForm.timeDuration + 1;
  193. this.transcriptionData.content +=
  194. _copyPrivSpeakerId + ":" + privText + "\n";
  195. this.onRecordAddLine(newItem);
  196. };
  197. iiframe.contentWindow.ConversationTranscriber();
  198. },
  199. onFinishRecordWithMicrosoft() {
  200. if (this.recordedForm.status == 1) {
  201. //正在录音时
  202. let iiframe = this.$refs["iiframe"];
  203. iiframe.contentWindow.window.document
  204. .getElementById("scenarioStopButton")
  205. .click();
  206. // 录音借宿
  207. iiframe.contentWindow.onSessionStopped = (s, e) => {
  208. this.recordedForm.status = 0;
  209. this.controlsStatus = 2;
  210. this.showGetTextLoading = false;
  211. this.$message.success("已结束录音");
  212. console.log("结束录音👇");
  213. console.log("结束录音", e);
  214. this.recordedForm.audioBlob.push(e.preaudio);
  215. let blob = new Blob(this.recordedForm.audioBlob, {
  216. type: "audio/wav"
  217. });
  218. let file = new File([blob], "recordedFile.wav", {
  219. type: "audio/wav"
  220. });
  221. // 存储文件和文本到全局对象
  222. this.storeRecordingData(file);
  223. iiframe.contentWindow.onSessionStopped = null;
  224. iiframe.contentWindow.window.onRecognizedResult = null;
  225. };
  226. } else if (this.recordedForm.status == 2) {
  227. //暂停录音时
  228. this.recordedForm.status = 0;
  229. this.controlsStatus = 2;
  230. this.showGetTextLoading = false;
  231. let blob = new Blob(this.recordedForm.audioBlob, {
  232. type: "audio/wav"
  233. });
  234. let file = new File([blob], "recordedFile.wav", { type: "audio/wav" });
  235. // 存储文件和文本到全局对象
  236. this.storeRecordingData(file);
  237. }
  238. },
  239. storeRecordingData(file) {
  240. // 配置全局 window 对象存储录音数据
  241. if (!window.recordingData) {
  242. window.recordingData = {};
  243. }
  244. window.recordingData.file = file;
  245. window.recordingData.text = this.transcriptionData.content;
  246. window.recordingData.textList = this.recordedForm.textList;
  247. console.log("录音数据已存储到全局对象:", window.recordingData);
  248. },
  249. updateRecordedTime({ duration }) {
  250. // 格式化录音时间
  251. const minutes = Math.floor(duration / 60);
  252. const seconds = Math.floor(duration % 60);
  253. return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  254. },
  255. onRecordAddLine(item) {
  256. // 添加录音文本行
  257. console.log("添加录音文本行:", item);
  258. // 这里可以根据需要添加更多处理逻辑
  259. },
  260. getCourseDetail() {
  261. this.pageLoading = true;
  262. let params = {
  263. courseId: this.id
  264. };
  265. this.ajax
  266. .get(this.$store.state.api + "selectCourseDetail3", params)
  267. .then(res => {
  268. console.log("getCourseDetail", res);
  269. this.courseDetail = res.data[0][0];
  270. this.courseDetail.chapters = JSON.parse(this.courseDetail.chapters);
  271. this.tcid = this.arrayToArray(
  272. this.courseDetail.juri ? this.courseDetail.juri.split(",") : [],
  273. this.tcid2 ? this.tcid2.split(",") : []
  274. )[0] || "";
  275. if (this.tcid && res.data[1].length) {
  276. let _inviteA = [];
  277. for (var ik = 0; ik < res.data[1].length; ik++) {
  278. _inviteA.push({
  279. cid: res.data[1][ik].classid,
  280. ic: res.data[1][ik].code,
  281. });
  282. }
  283. for (var ik = 0; ik < _inviteA.length; ik++) {
  284. if (
  285. this.arrayToArray(
  286. _inviteA[ik].cid.split(","),
  287. this.tcid.split(",")
  288. ).length
  289. ) {
  290. this.inviteCode = _inviteA[ik].ic;
  291. break;
  292. }
  293. }
  294. }
  295. this.setPptIframe()
  296. this.pageLoading = false;
  297. })
  298. .catch(err => {
  299. console.log(err);
  300. this.$message.error("获取课程数据失败");
  301. this.pageLoading = false;
  302. });
  303. },
  304. setPptIframe() {
  305. this.showIframe = false;
  306. this.$nextTick(() => {
  307. let api = 'https://beta.ppt.cocorobo.cn'
  308. if (this.$region == 'beta') {
  309. api = 'https://beta.ppt.cocorobo.cn'
  310. } else if (this.$region == 'hk') {
  311. api = 'https://ppt.cocorobo.hk'
  312. } else if (this.$region == 'com') {
  313. api = 'https://ppt.cocorobo.com'
  314. } else {
  315. api = 'https://ppt.cocorobo.cn'
  316. }
  317. let _url = api + `/?mode=student&courseid=${this.id}&userid=${this.userid}&oid=${this.oid}&org=${this.org}&cid=${this.tcid}&type=${this.tType}`;
  318. this.iframeSrc = _url;
  319. this.showIframe = true;
  320. });
  321. },
  322. arrayToArray(arrayo, arrayt) {
  323. let array1 = arrayo;
  324. let array2 = arrayt;
  325. let commonElements = [];
  326. for (let i = 0; i < array1.length; i++) {
  327. for (let j = 0; j < array2.length; j++) {
  328. if (array1[i] === array2[j]) {
  329. commonElements.push(array1[i]);
  330. }
  331. }
  332. }
  333. return commonElements;
  334. },
  335. async getClassName() {
  336. let courseGrade = await this.ajax.get(this.$store.state.api + "getClassById", { id: this.tcid2 });
  337. this.className = courseGrade.data[0][0].grade;
  338. },
  339. back() {
  340. if (this.tType != 2) {
  341. this.goTo(
  342. '/courseDetail?userid=' +
  343. this.userid +
  344. '&oid=' +
  345. this.oid +
  346. '&org=' +
  347. this.org +
  348. '&cid=' +
  349. this.classId +
  350. '&courseId=' +
  351. this.id +
  352. '&tType=' +
  353. this.tType +
  354. '&screenType=' +
  355. this.screenType
  356. )
  357. } else {
  358. this.goTo(
  359. '/index?userid=' +
  360. this.userid +
  361. '&oid=' +
  362. this.oid +
  363. '&org=' +
  364. this.org +
  365. '&cid=' +
  366. this.classId +
  367. '&tType=' +
  368. this.tType +
  369. '&screenType=' +
  370. this.screenType
  371. )
  372. }
  373. },
  374. afterClass() {
  375. this.$confirm('此操作将使当前课程内所有学生退出登录,是否继续?', '提示', {
  376. confirmButtonText: '确定',
  377. cancelButtonText: '取消',
  378. type: 'warning'
  379. }).then(() => {
  380. this.$refs.ppt.contentWindow.PPTistStudent.forceLogout();
  381. }).catch(() => {});
  382. },
  383. onFreeBrowseChange(value) {
  384. this.freeBrowse = value;
  385. console.log('自由浏览模式已切换为1:', this.freeBrowse);
  386. this.$refs.ppt.contentWindow.PPTistStudent.toggleFollowMode()
  387. },
  388. setOperationTime() {
  389. let _this = this;
  390. if (_this.opertimer) {
  391. clearInterval(_this.opertimer);
  392. _this.opertimer = null;
  393. }
  394. _this.opertimer = setInterval(() => {
  395. _this.setoTime("600");
  396. }, 600000);
  397. },
  398. setoTime(time) {
  399. let params = [
  400. {
  401. uid: this.userid,
  402. cid: this.id,
  403. type: "2",
  404. time: time,
  405. },
  406. ];
  407. this.ajax
  408. .post(this.$store.state.api + "addOperationTimeT2", params)
  409. .then((res) => {})
  410. .catch((err) => {
  411. console.error(err);
  412. });
  413. },
  414. getAIJ(){
  415. this.ajax.get(this.$store.state.api+"getAIJ","").then(res=>{
  416. let a = res.data[4];
  417. console.log(a)
  418. let Array = [];
  419. a.forEach(i=>Array.push(i.oid))
  420. this.jArray = Array;
  421. })
  422. },
  423. },
  424. destroyed() {
  425. clearInterval(this.opertimer);
  426. this.opertimer = null;
  427. if (this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840') {
  428. let endTime = new Date().toLocaleString("zh-CN", {
  429. hour12: false,
  430. timeZone: "Asia/Shanghai"
  431. }).replace(/\//g, "-")
  432. let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
  433. this.syncClassData({
  434. courseId: this.id,
  435. title: this.courseDetail.title,
  436. courseGrade: this.tcid2 ? this.tcid2 : '',
  437. courseTime: courseTime,
  438. startTime: this.startTime,
  439. endTime: endTime,
  440. })
  441. console.log('同步数据')
  442. }
  443. },
  444. async mounted() {
  445. this.setoTime("1");
  446. this.startTime = new Date().toLocaleString("zh-CN", {
  447. hour12: false,
  448. timeZone: "Asia/Shanghai"
  449. }).replace(/\//g, "-")
  450. this.getClassName()
  451. this.getAIJ();
  452. this.getCourseDetail();
  453. this.setOperationTime();
  454. window.onFreeBrowseChange = (value) => {
  455. this.freeBrowse = value;
  456. console.log('自由浏览模式已切换为:', this.freeBrowse);
  457. }
  458. if(!this.userJson || !this.userJson.accountNumber){
  459. let res = await this.ajax.get(this.$store.state.api + "selectUser", {
  460. userid: this.$route.query.userid
  461. });
  462. this.userJson = res.data[0][0]
  463. }
  464. }
  465. };
  466. </script>
  467. <style scoped>
  468. .pptEasyClass {
  469. width: 100vw;
  470. height: 100vh;
  471. display: flex;
  472. flex-direction: column;
  473. overflow: hidden;
  474. box-sizing: border-box;
  475. background-color: #f2f2f2;
  476. min-height: unset;
  477. }
  478. .pec_main {
  479. width: 100%;
  480. height: 100%;
  481. background-color: #fff;
  482. }
  483. .pec_header {
  484. width: 100%;
  485. height: 60px;
  486. background: #FCCF00;
  487. box-sizing: border-box;
  488. display: flex;
  489. align-items: center;
  490. justify-content: space-between;
  491. position: relative;
  492. box-shadow: 0px 4px 12px 0px #3648601F;
  493. padding: 0 10px;
  494. box-sizing: border-box;
  495. }
  496. .pec_h_left {
  497. width: auto;
  498. height: 100%;
  499. display: flex;
  500. align-items: center;
  501. gap: 25px;
  502. /* 保持左侧靠左 */
  503. }
  504. .pec_h_center {
  505. position: absolute;
  506. left: 50%;
  507. top: 0;
  508. height: 100%;
  509. display: flex;
  510. align-items: center;
  511. transform: translateX(-50%);
  512. z-index: 1;
  513. }
  514. .pec_h_l_title {
  515. font-weight: bold;
  516. font-size: 20px;
  517. color: #0e1e33;
  518. overflow: hidden;
  519. white-space: nowrap;
  520. max-width: 500px;
  521. text-overflow: ellipsis;
  522. }
  523. .pec_h_right {
  524. width: auto;
  525. height: 100%;
  526. display: flex;
  527. align-items: center;
  528. }
  529. .pec_h_r_btnArea {
  530. display: flex;
  531. align-items: center;
  532. justify-content: center;
  533. }
  534. .pec_h_r_btnArea>div {
  535. width: auto;
  536. height: auto;
  537. display: flex;
  538. align-items: center;
  539. justify-content: center;
  540. padding: 10px 20px;
  541. background-color: #f0f4fa;
  542. border-radius: 4px;
  543. cursor: pointer;
  544. font-size: 14px;
  545. font-weight: 400;
  546. color: #000;
  547. border: 1px solid #cad1dc;
  548. }
  549. .pec_h_r_btnArea> div + div {
  550. margin-left: 10px;
  551. }
  552. .pec_h_r_btnArea>div>img {
  553. width: 15px;
  554. height: 15px;
  555. margin-right: 5px;
  556. }
  557. .pec_h_r_btnArea>.pec_h_r_btn_refresh {
  558. color: #fff;
  559. background-color: #0061ff;
  560. border-color: #0061ff;
  561. }
  562. .pec_h_r_btnArea>.pec_h_r_btn_refresh.recording {
  563. background-color: #F53F3F;
  564. border-color: #F53F3F;
  565. animation: pulse 1.5s infinite;
  566. }
  567. @keyframes pulse {
  568. 0% {
  569. box-shadow: 0 0 0 0 rgba(245, 63, 63, 0.4);
  570. }
  571. 70% {
  572. box-shadow: 0 0 0 10px rgba(245, 63, 63, 0);
  573. }
  574. 100% {
  575. box-shadow: 0 0 0 0 rgba(245, 63, 63, 0);
  576. }
  577. }
  578. .pec_h_r_btnArea>.pec_h_r_btn_afterClass {
  579. border-color: #F0E1DD;
  580. background-color: #FFF7F5;
  581. color: #F53F3F;
  582. }
  583. .backBtn {
  584. width: 15px;
  585. height: 15px;
  586. display: flex;
  587. align-items: center;
  588. justify-content: center;
  589. cursor: pointer;
  590. }
  591. .backBtn img {
  592. width: 100%;
  593. height: 100%;
  594. }
  595. .class-info-group {
  596. display: flex;
  597. align-items: center;
  598. gap: 10px;
  599. }
  600. .class-label {
  601. font-size: 18px;
  602. font-weight: bold;
  603. color: #222;
  604. margin-right: 5px;
  605. }
  606. .class-value {
  607. font-size: 16px;
  608. color: #222;
  609. background: #FFFFFF3D;
  610. border: 1px solid #00000080;
  611. border-radius: 5px;
  612. padding: 5px 18px;
  613. min-width: 60px;
  614. text-align: center;
  615. display: inline-block;
  616. box-sizing: border-box;
  617. }
  618. .pec_content {
  619. width: 100%;
  620. height: calc(100% - 60px);
  621. border-radius: 0 0 12px 12px;
  622. background-color: #fff;
  623. }
  624. .free-browse-switch {
  625. display: flex;
  626. align-items: center;
  627. padding: 9px 10px;
  628. background: #FFF7F5;
  629. border-radius: 26px;
  630. margin-left: 15px;
  631. gap: 5px;
  632. }
  633. .switch-label {
  634. /* background: linear-gradient(to right, #F53F3F, #FCCF00); */
  635. /* -webkit-background-clip: text; */
  636. color: #000;
  637. }
  638. .refresh_icon {
  639. width: 40px;
  640. height: 40px;
  641. display: flex;
  642. align-items: center;
  643. justify-content: center;
  644. background-color: #fff7f5;
  645. border-radius: 45%;
  646. cursor: pointer;
  647. margin-left: 15px;
  648. padding: 0 3px;
  649. }
  650. .refresh_icon img {
  651. width: 20px;
  652. height: 20px;
  653. }
  654. .name_box {
  655. background: unset !important;
  656. border: none !important;
  657. cursor: unset !important;
  658. }
  659. </style>