index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. <template>
  2. <div class="ch_box" ref="ch_box">
  3. <div class="ch_content_box" v-show="type == 1">
  4. <searchArea
  5. :courseDetail="courseDetail"
  6. ref="searchAreaRef"
  7. :navList="navList"
  8. :tcid="tcid"
  9. :openMegaphone="openMegaphone"
  10. :fileId="fileId"
  11. :recordType="recordType"
  12. v-if="itemType == 1"
  13. />
  14. <taskArea
  15. :courseDetail="courseDetail"
  16. :navList="navList"
  17. :courseType="courseType"
  18. :taskCount="taskCount"
  19. :worksStudent="worksStudent"
  20. :openMegaphone="openMegaphone"
  21. ref="taskAreaRef"
  22. :fileId="fileId"
  23. v-if="itemType == 2"
  24. />
  25. <countdown
  26. ref="countdownRef"
  27. :fileId="fileId"
  28. :courseDetail="courseDetail"
  29. v-show="itemType == 3"
  30. />
  31. <languageAssistant ref="languageAssistantRef" v-if="itemType == 4" />
  32. <!-- <dialogArea
  33. :courseDetail="courseDetail"
  34. :openMegaphone="openMegaphone"
  35. ref="dialogAreaRef"
  36. :fileId="fileId"
  37. v-if="itemType == 3"
  38. /> -->
  39. </div>
  40. <div class="ch_nav_box">
  41. <div class="ch_nav_box_top">
  42. <div @click.stop="changeFold(!fold)" ref="foldBtnRef">
  43. <el-tooltip
  44. class="item"
  45. effect="dark"
  46. :content="fold ? '折叠' : '展开'"
  47. placement="top"
  48. >
  49. <img
  50. :src="require('../../assets/icon/course/foldIcon.svg')"
  51. alt=""
  52. :class="[!fold?'':'foldActive']"
  53. />
  54. </el-tooltip>
  55. </div>
  56. <div @click="$emit('review')" v-if="tType == 1">
  57. <el-tooltip class="item" effect="dark" content="评论" placement="top">
  58. <img
  59. :src="require('../../assets/icon/course/comment2.svg')"
  60. alt=""
  61. style="width: 22px;height: 22px;"
  62. />
  63. </el-tooltip>
  64. </div>
  65. <div
  66. @click="startRecording()"
  67. v-if="!videoStart && (tType == 1 || tType == 4)"
  68. >
  69. <el-tooltip class="item" effect="dark" content="录制" placement="top">
  70. <img
  71. :src="require('../../assets/icon/course/record3.svg')"
  72. alt=""
  73. style="width: 22px;height: 22px;"
  74. />
  75. </el-tooltip>
  76. </div>
  77. <div
  78. @click="$emit('stopRecording')"
  79. v-else-if="tType == 1 || tType == 4"
  80. style="background:#f63564"
  81. >
  82. <el-tooltip class="item" effect="dark" content="下载" placement="top">
  83. <img
  84. :src="require('../../assets/icon/course/record4.svg')"
  85. alt=""
  86. style="width: 22px;height: 22px;"
  87. />
  88. </el-tooltip>
  89. </div>
  90. </div>
  91. <div class="ch_nav_box_middle">
  92. <div
  93. :class="[
  94. 'ch_nav_box_middle_item',
  95. itemType == 2 ? 'ch_nav_box_middle_item_active' : ''
  96. ]"
  97. @click.stop="changeItemType(2)"
  98. >
  99. <img
  100. v-if="itemType == 2"
  101. :src="require('../../assets/icon/course/task_active.png')"
  102. />
  103. <img
  104. v-if="itemType != 2"
  105. :src="require('../../assets/icon/course/task.png')"
  106. />
  107. <!-- <span :style="`background:url(${itemType==2?require('../../assets/icon/course/task_active.png'):require('../../assets/icon/course/task.png')});`"></span> -->
  108. <div>任务</div>
  109. </div>
  110. <div
  111. :class="[
  112. 'ch_nav_box_middle_item',
  113. itemType == 1 ? 'ch_nav_box_middle_item_active' : ''
  114. ]"
  115. @click.stop="changeItemType(1)"
  116. >
  117. <img
  118. v-if="itemType == 1"
  119. :src="require('../../assets/icon/course/up_active.png')"
  120. />
  121. <img
  122. v-if="itemType != 1"
  123. :src="require('../../assets/icon/course/up.png')"
  124. />
  125. <!-- <span :style="`background:url(${itemType==1?require('../../assets/icon/course/up_active.png'):require('../../assets/icon/course/up.png')});`"></span> -->
  126. <div>对话</div>
  127. </div>
  128. <div
  129. :class="[
  130. 'ch_nav_box_middle_item',
  131. itemType == 3 ? 'ch_nav_box_middle_item_active' : ''
  132. ]"
  133. @click.stop="changeItemType(3)"
  134. >
  135. <img
  136. v-if="itemType == 3"
  137. :src="require('../../assets/icon/course/Countdown2.svg')"
  138. />
  139. <img
  140. v-if="itemType != 3"
  141. :src="require('../../assets/icon/course/Countdown.svg')"
  142. />
  143. <div>倒计时</div>
  144. </div>
  145. </div>
  146. <div class="ch_nav_box_bottom">
  147. <div @click.stop="commentAndAnnotate()">
  148. <el-tooltip class="item" effect="dark" content="批注" placement="top">
  149. <img
  150. :src="require('../../assets/icon/course/edit2.svg')"
  151. v-if="!AnnotationCanvasShow"
  152. />
  153. <img :src="require('../../assets/icon/course/edit3.svg')" v-else />
  154. </el-tooltip>
  155. </div>
  156. <!-- <div @click.stop="changeItemType(4)" :class="[itemType == 4?'ch_nav_box_middle_item_active':'']">
  157. <el-tooltip
  158. class="item"
  159. effect="dark"
  160. :content="itemType != 4?'开启语音助手':'关闭语音助手'"
  161. placement="top"
  162. >
  163. <img v-if="itemType != 4" :src="require('../../assets/icon/course/robot.svg')" />
  164. <img v-else :src="require('../../assets/icon/course/robot2.svg')" />
  165. </el-tooltip>
  166. </div> -->
  167. <div
  168. @click.stop="startAssistant()"
  169. :class="[recordType == 1 ? 'ch_nav_box_middle_item_active' : '']"
  170. >
  171. <el-tooltip
  172. class="item"
  173. effect="dark"
  174. :content="recordType == 0 ? '开启语音助手' : '关闭语音助手'"
  175. placement="top"
  176. >
  177. <img
  178. v-if="recordType != 1"
  179. :src="require('../../assets/icon/course/robot3.svg')"
  180. />
  181. <img v-else :src="require('../../assets/icon/course/robot3.svg')" />
  182. </el-tooltip>
  183. </div>
  184. <div @click.stop="$emit('goStep', 0)">
  185. <el-tooltip
  186. class="item"
  187. effect="dark"
  188. content="上一步"
  189. placement="top"
  190. >
  191. <img :src="require('../../assets/icon/course/last.png')" />
  192. </el-tooltip>
  193. </div>
  194. <div @click.stop="$emit('goStep', 1)">
  195. <el-tooltip
  196. class="item"
  197. effect="dark"
  198. content="下一步"
  199. placement="top"
  200. >
  201. <img :src="require('../../assets/icon/course/next.png')" />
  202. </el-tooltip>
  203. </div>
  204. <div @click="openSetting">
  205. <el-tooltip
  206. class="item"
  207. effect="dark"
  208. :content="type == 0 ? '展开' : '折叠'"
  209. placement="top"
  210. >
  211. <img :src="require('../../assets/icon/course/menu.png')" />
  212. </el-tooltip>
  213. </div>
  214. </div>
  215. </div>
  216. <div v-if="fold" class="itemFold" ref="itemFoldRef" v-click-outside="handleBlur">
  217. <div @click="$emit('backPage')">
  218. <img :src="require('../../assets/icon/course/return.png')" alt="" />
  219. <span>返回</span>
  220. </div>
  221. <div @click="$emit('refresh')">
  222. <img :src="require('../../assets/icon/course/refresh.png')" alt="" />
  223. <span>刷新</span>
  224. </div>
  225. <div @click="$emit('authority')" v-if="tType == 1 || tType == 4">
  226. <img :src="require('../../assets/icon/course/setting2.svg')" alt="" />
  227. <span>权限</span>
  228. </div>
  229. </div>
  230. <levitatedSphere ref="levitatedSphereRef" @startTime="startTime" />
  231. <timepiece ref="timepieceRef" />
  232. <AnnotationCanvas
  233. ref="AnnotationCanvasRef"
  234. @close="endCommentAndAnnotate"
  235. @changeStatus="changeAnnotationCanvasShow"
  236. />
  237. </div>
  238. </template>
  239. <script>
  240. import searchArea from "./component/searchArea.vue";
  241. import taskArea from "./component/taskArea.vue";
  242. // import dialogArea from "./component/dialogArea.vue";
  243. import levitatedSphere from "./component/levitatedSphere.vue";
  244. import timepiece from "./component/timepiece.vue";
  245. import countdown from "./component/countdown.vue";
  246. import AnnotationCanvas from "./component/AnnotationCanvas.vue";
  247. import languageAssistant from "./component/languageAssistant.vue";
  248. // 自定义指令,用于处理点击外部区域的事件
  249. const clickOutside = {
  250. bind(el, binding) {
  251. // 在元素上绑定一个点击事件监听器
  252. el.clickOutsideEvent = function (event) {
  253. // 检查点击事件是否发生在元素的内部
  254. if (!(el === event.target || el.contains(event.target))) {
  255. // 如果点击事件发生在元素的外部,则触发指令绑定的方法,将点击的event数据传过去
  256. binding.value(event);
  257. }
  258. };
  259. // 在文档上添加点击事件监听器
  260. document.addEventListener("click", el.clickOutsideEvent);
  261. },
  262. unbind(el) {
  263. // 在元素上解除点击事件监听器
  264. document.removeEventListener("click", el.clickOutsideEvent);
  265. },
  266. };
  267. export default {
  268. emits: [
  269. "refresh",
  270. "goStep",
  271. "backPage",
  272. "authority",
  273. "review",
  274. "stopRecording",
  275. "startRecording"
  276. ],
  277. directives: {
  278. "click-outside": clickOutside, // 注册自定义指令
  279. },
  280. components: {
  281. searchArea,
  282. taskArea,
  283. // dialogArea,
  284. levitatedSphere,
  285. timepiece,
  286. countdown,
  287. AnnotationCanvas,
  288. languageAssistant
  289. },
  290. props: {
  291. courseDetail: {
  292. type: Object,
  293. default: () => {}
  294. },
  295. tType: {
  296. type:String,
  297. default: 0
  298. },
  299. navList: {
  300. type: Array,
  301. default: () => []
  302. },
  303. tcid: {
  304. type: String,
  305. default: ""
  306. },
  307. courseType: {
  308. type: Number,
  309. default: 0
  310. },
  311. taskCount: {
  312. type: Number,
  313. default: 0
  314. },
  315. worksStudent: {
  316. type: Array,
  317. default: () => []
  318. },
  319. fileList: {
  320. type: Array,
  321. default: () => []
  322. },
  323. videoStart: {
  324. type: Boolean,
  325. default: false
  326. }
  327. },
  328. data() {
  329. return {
  330. userid: this.$route.query.userid,
  331. courseId: this.$route.query.courseId,
  332. tcid2: this.$route.query.tcid,
  333. type: 0,
  334. itemType: 0, //0--无 1-搜索 2-任务 3-对话
  335. fileId: [],
  336. recordType: 0,
  337. recordLoading: false,
  338. fold: false,
  339. openMegaphone: false, //是否打开喇叭
  340. getFileIdLoading: false,
  341. AnnotationCanvasShow: false
  342. };
  343. },
  344. mounted() {
  345. this.setWidth();
  346. this.getFileId();
  347. },
  348. methods: {
  349. handleBlur(){
  350. // console.log(this.fold)
  351. this.fold = !this.fold;
  352. },
  353. changeAnnotationCanvasShow(newValue) {
  354. this.AnnotationCanvasShow = newValue;
  355. },
  356. startRecording() {
  357. this.$emit("startRecording");
  358. this.insertMemorandum(`使用<span class="btn">录制</span>功能,录制课堂`);
  359. },
  360. insertMemorandum(_html) {
  361. //保存行为操作
  362. //variable
  363. //btn
  364. let params = [
  365. {
  366. uid: this.userid,
  367. courseId: this.courseId+(this.tcid2?this.tcid2:""),
  368. content: _html
  369. }
  370. ];
  371. this.ajax
  372. .post(
  373. this.$store.state.api + "insert_systemOperation_countdownBehavior",
  374. params
  375. )
  376. .then(res => {
  377. if (res.data == 1) {
  378. console.log("保存操作成功");
  379. } else {
  380. console.log("保存操作失败");
  381. }
  382. })
  383. .catch(e => {
  384. console.log("保存操作失败");
  385. console.log(e);
  386. });
  387. },
  388. setWidth() {
  389. let w = this.$refs.ch_box;
  390. let w2 = w.offsetWidth + 30 + "px";
  391. this.$emit("setWidth", w2);
  392. },
  393. openSetting() {
  394. this.type = this.type == 1 ? 0 : 1;
  395. this.$nextTick(() => {
  396. if(this.type==1){
  397. this.$parent.mlDialog = false
  398. }
  399. this.setWidth();
  400. });
  401. },
  402. changeItemType(type) {
  403. this.type = 0;
  404. this.openSetting();
  405. // this.$message.info("切换到"+type)
  406. this.$nextTick(() => {
  407. // if (this.itemType == 1 && type != 1) {
  408. // this.$refs.searchAreaRef.scrollBottom();
  409. // this.$refs.searchAreaRef.getWantSearch();
  410. // } else if (this.itemType == 2) {
  411. // this.$refs.taskAreaRef.scrollBottom();
  412. // } else if (this.itemType == 3) {
  413. // this.$refs.dialogAreaRef.scrollBottom();
  414. // }
  415. this.itemType = type;
  416. if (this.itemType == 4 && this.recordType == 1) {
  417. //关闭悬浮语音助手
  418. this.$refs.levitatedSphereRef.stopRecord();
  419. }
  420. if (this.itemType == 3) {
  421. this.insertMemorandum(`打开<span class="btn">倒计时</span>面板`);
  422. }
  423. });
  424. },
  425. //计时
  426. startTime(time) {
  427. this.$refs.timepieceRef.startTime(time);
  428. },
  429. getFileId() {
  430. if (this.getFileIdLoading) return;
  431. this.getFileIdLoading = true;
  432. this.fileId = [];
  433. let _this = this;
  434. let _successFileUrl = [];
  435. if (this.fileList.length <= 0) retrun;
  436. let addType = ["DOCX", "DOC", "PPT", "PPTX", "MD", "TXT", "PDF"];
  437. this.fileList.forEach(i => {
  438. if (
  439. addType.includes(
  440. i.url.split(".")[i.url.split(".").length - 1].toLocaleUpperCase()
  441. )
  442. ) {
  443. _successFileUrl.push(i.url);
  444. }
  445. });
  446. let promiseList = [];
  447. _successFileUrl.forEach(i => {
  448. promiseList.push(
  449. new Promise((resolve, reject) => {
  450. _this.ajax
  451. .put("https://gpt4.cocorobo.cn/upload_file_knowledge", {
  452. url: i
  453. })
  454. .then(res => {
  455. let _data = res.data.FunctionResponse;
  456. if (_data.result && _data.result.id) {
  457. this.fileId.push(_data.result.id);
  458. }
  459. resolve();
  460. });
  461. })
  462. );
  463. });
  464. Promise.all(promiseList).then(res => {
  465. this.getFileIdLoading = false;
  466. });
  467. },
  468. startAssistant() {
  469. if (this.recordLoading) return this.$message.info("请稍等...");
  470. this.recordLoading = true;
  471. if (this.recordType == 0) {
  472. if (this.itemType == 4) {
  473. this.itemType = 0;
  474. this.type = 0;
  475. }
  476. // this.$message.info("开启")
  477. // this.changeRecordType(1)
  478. this.$refs.levitatedSphereRef.recordStart();
  479. } else if (this.recordType == 1) {
  480. // this.$message.info("关闭")
  481. // this.changeRecordType(0)
  482. this.$refs.levitatedSphereRef.stopRecord();
  483. }
  484. },
  485. changeMegaphone() {
  486. this.openMegaphone = !this.openMegaphone;
  487. if (this.openMegaphone) {
  488. this.$message.success("已开启AI语音");
  489. } else {
  490. this.$message.success("已关闭AI语音");
  491. }
  492. },
  493. // 展开
  494. changeFold(newValue) {
  495. // this.$message.info("展开");
  496. this.fold = newValue;
  497. this.$nextTick(()=>{
  498. let e1 = this.$refs.foldBtnRef.getBoundingClientRect();
  499. let e2 = this.$refs.ch_box.getBoundingClientRect();
  500. console.log('👇👇')
  501. console.log(e1.top - e2.top)
  502. this.$refs.itemFoldRef.style.top = e1.top - e2.top + "px";
  503. })
  504. },
  505. // 收起
  506. changeUnfold(newValue) {
  507. // this.$message.info("收起");
  508. this.fold = newValue;
  509. },
  510. // 语音识别
  511. startRecord() {
  512. this.$refs.levitatedSphereRef.startRecord();
  513. },
  514. stopRecord() {
  515. this.$refs.levitatedSphereRef.stopRecord();
  516. },
  517. // 语音合成
  518. startSpeak() {
  519. },
  520. changeRecordType(type) {
  521. this.recordLoading = false;
  522. this.recordType = type;
  523. },
  524. commentAndAnnotate() {
  525. this.$refs.AnnotationCanvasRef.open();
  526. this.insertMemorandum(`开始使用<span class="btn">批注</span>功能`);
  527. },
  528. endCommentAndAnnotate() {
  529. this.insertMemorandum(`结束使用<span class="btn">批注</span>功能`);
  530. }
  531. }
  532. };
  533. </script>
  534. <style scoped>
  535. .ch_box {
  536. width: auto;
  537. background: rgb(255, 255, 255);
  538. position: fixed;
  539. height: calc(100% - 40px);
  540. border-radius: 10px;
  541. box-sizing: border-box;
  542. right: 20px;
  543. display: flex;
  544. top: 20px;
  545. z-index: 1000;
  546. }
  547. .ch_nav_box {
  548. height: 100%;
  549. width: 65px;
  550. display: flex;
  551. flex-direction: column;
  552. align-items: center;
  553. overflow-y: auto !important; /* 上下溢出显示滚动条 */
  554. overflow-x: hidden !important; /* 左右溢出正常溢出 */
  555. position: relative;
  556. }
  557. .ch_content_box {
  558. width: 400px;
  559. height: 100%;
  560. border-right: 2px solid #e7e7e7;
  561. }
  562. .ch_nav_box_bottom {
  563. width: 100%;
  564. box-sizing: border-box;
  565. border-top: solid 1px #eaeaea;
  566. display: flex;
  567. flex-direction: column;
  568. }
  569. .ch_nav_box_middle {
  570. width: 100%;
  571. box-sizing: border-box;
  572. border-top: solid 1px #eaeaea;
  573. flex-direction: column;
  574. justify-content: space-between;
  575. }
  576. .ch_nav_box_middle_item {
  577. width: 100%;
  578. height: 80px;
  579. display: flex;
  580. flex-direction: column;
  581. justify-content: center;
  582. align-items: center;
  583. cursor: pointer;
  584. transition: 0.3s;
  585. font-size: 14px;
  586. }
  587. .ch_nav_box_middle_item_active {
  588. background-color: #3681fc;
  589. color: white;
  590. }
  591. .ch_nav_box_middle_item > img {
  592. width: 24px;
  593. height: 24px;
  594. margin-bottom: 5px;
  595. }
  596. .ch_nav_box_bottom > div {
  597. width: 100%;
  598. height: 65px;
  599. display: flex;
  600. flex-direction: column;
  601. justify-content: center;
  602. align-items: center;
  603. cursor: pointer;
  604. }
  605. .ch_nav_box_bottom > div > img {
  606. width: 24px;
  607. height: 24px;
  608. }
  609. .ch_nav_box_top {
  610. width: 100%;
  611. height: auto;
  612. margin-top: auto;
  613. }
  614. .ch_nav_box_top > div {
  615. width: 100%;
  616. height: 65px;
  617. display: flex;
  618. justify-content: center;
  619. align-items: center;
  620. cursor: pointer;
  621. position: relative;
  622. }
  623. .ch_nav_box_top > div > img {
  624. width: 24px;
  625. height: 24px;
  626. transition: 0.3s;
  627. }
  628. .itemFold {
  629. position: absolute;
  630. width: 130px;
  631. right: 65px;
  632. top: 0;
  633. background: rgb(255, 255, 255);
  634. box-sizing: border-box;
  635. border: solid 1px #eaeaea;
  636. /* border-top: solid 1px #eaeaea; */
  637. z-index: 1000; /* 确保二级菜单在主菜单上层 */
  638. border-radius: 10px;
  639. box-sizing: border-box;
  640. padding:10px;
  641. display:flex;
  642. flex-direction: column;
  643. justify-content: center;
  644. align-items: center;
  645. }
  646. .itemFold > div {
  647. width: 100%;
  648. height: 45px;
  649. display: flex;
  650. justify-content: center;
  651. align-items: center;
  652. cursor: pointer;
  653. margin: 5px 0;
  654. border-radius: 10px;
  655. }
  656. .itemFold > div:hover{
  657. background:#F3F7FD;
  658. }
  659. .itemFold > div > img {
  660. width: 24px;
  661. height: 24px;
  662. }
  663. .itemFold > div > span{
  664. margin-left:10px;
  665. }
  666. @media screen and (max-height: 820px) {
  667. .ch_nav_box_bottom > div {
  668. height: auto;
  669. padding: 8px 0;
  670. }
  671. .ch_nav_box_middle_item{
  672. height: auto;
  673. padding: 8px 0;
  674. }
  675. .ch_nav_box_top > div{
  676. height: auto;
  677. padding: 8px 0;
  678. }
  679. }
  680. .foldActive{
  681. background-color: #F0F2F5;
  682. padding: 10px;
  683. border-radius: 10px;
  684. }
  685. </style>