From 44092a21bf60356c528d2980abaa19db76d26b8c Mon Sep 17 00:00:00 2001 From: zdg Date: Mon, 9 Dec 2024 13:51:00 +0800 Subject: [PATCH] =?UTF-8?q?ppt=E4=B8=8A=E8=AF=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 5 + .env.production | 4 + src/renderer/src/AixPPTist/src/App.vue | 1 + .../src/AixPPTist/src/api/classcourse.ts | 22 +++ src/renderer/src/AixPPTist/src/api/index.ts | 2 +- src/renderer/src/AixPPTist/src/api/types.ts | 16 ++ .../src/AixPPTist/src/hooks/useScreening.ts | 9 +- .../src/AixPPTist/src/store/classcourse.ts | 18 ++ src/renderer/src/AixPPTist/src/store/index.ts | 2 + src/renderer/src/plugins/socket/index.js | 164 ++++++++++++++++++ src/renderer/src/views/model/index.vue | 19 +- .../views/prepare/container/class-start.vue | 47 +++-- src/renderer/src/views/prepare/index.vue | 7 +- 13 files changed, 295 insertions(+), 21 deletions(-) create mode 100644 src/renderer/src/AixPPTist/src/api/classcourse.ts create mode 100644 src/renderer/src/AixPPTist/src/store/classcourse.ts create mode 100644 src/renderer/src/plugins/socket/index.js diff --git a/.env.development b/.env.development index f3b45f2..b3a7114 100644 --- a/.env.development +++ b/.env.development @@ -16,4 +16,9 @@ VITE_APP_RES_FILE_PATH = 'https://file.ysaix.com:7868/src/assets/textbook/booktx VITE_APP_BUILD_BASE_PATH = 'https://file.ysaix.com:7868/' +# websocket 地址 +# VITE_APP_WS_URL = 'wss://file.ysaix.com:7868' +VITE_APP_WS_URL = 'ws://192.168.2.16:7865' + +# 是否显示开发工具 VITE_SHOW_DEV_TOOLS = 'true' diff --git a/.env.production b/.env.production index e103054..0919367 100644 --- a/.env.production +++ b/.env.production @@ -18,4 +18,8 @@ VITE_APP_RES_FILE_PATH = 'https://prev.ysaix.com:7868/src/assets/textbook/booktx VITE_APP_BUILD_BASE_PATH = 'https://prev.ysaix.com:7868/' +# websocket 地址 +VITE_APP_WS_URL = 'wss://file.ysaix.com:7868' + +# 是否显示开发工具 VITE_SHOW_DEV_TOOLS = 'false' diff --git a/src/renderer/src/AixPPTist/src/App.vue b/src/renderer/src/AixPPTist/src/App.vue index 5214d9b..d18e59d 100644 --- a/src/renderer/src/AixPPTist/src/App.vue +++ b/src/renderer/src/AixPPTist/src/App.vue @@ -28,6 +28,7 @@ import * as API_entpcoursefile from '@/api/education/entpcoursefile' // 相关ap import { PPTApi } from './api' import { sessionStore } from '@/utils/store' // electron-store 状态管理 import './api/watcher' // 监听 +import './api/classcourse' // 课程相关 const loading = ref(true) const _isPC = isPC() diff --git a/src/renderer/src/AixPPTist/src/api/classcourse.ts b/src/renderer/src/AixPPTist/src/api/classcourse.ts new file mode 100644 index 0000000..ca162e1 --- /dev/null +++ b/src/renderer/src/AixPPTist/src/api/classcourse.ts @@ -0,0 +1,22 @@ +/** + * @author zdg + * @description 上课相关内容 + */ +import type { Classcourse } from './types' +import { sessionStore } from '@/utils/store' // electron-store 状态管理 +import * as useStore from '../store' // pptist-状态管理 +import { ChatWs } from '@/plugins/socket' // 聊天socket +const screenStore = useStore.useScreenStore() // 全屏-状态管理 +const classcourseStore = useStore.useClasscourseStore() // 课堂信息-状态管理 +const classcourse: Classcourse = sessionStore.get('curr.classcourse') // 课堂信息 + +// 如果课堂信息有值,则连接socket +if (!!classcourse) { + // 连接socket + const ws = new ChatWs() + console.log('ws- ',ws) + // ChatWs.connect(classcourse.id) + classcourseStore.setClasscourse(classcourse) +} +// 打开全屏 +screenStore.setScreening(!!classcourse) \ No newline at end of file diff --git a/src/renderer/src/AixPPTist/src/api/index.ts b/src/renderer/src/AixPPTist/src/api/index.ts index 32bb25a..b0c8239 100644 --- a/src/renderer/src/AixPPTist/src/api/index.ts +++ b/src/renderer/src/AixPPTist/src/api/index.ts @@ -136,7 +136,7 @@ export class PPTApi { id: currentSlide.id, datacontent: JSON.stringify(currentSlide), } - Utils.mxThrottle(() => {this.updateSlide(params)}, 1000, 2) + Utils.mxThrottle(() => {this.updateSlide(params)}, 200, 2) } } // 更新幻灯片 diff --git a/src/renderer/src/AixPPTist/src/api/types.ts b/src/renderer/src/AixPPTist/src/api/types.ts index 8321031..8d4f81c 100644 --- a/src/renderer/src/AixPPTist/src/api/types.ts +++ b/src/renderer/src/AixPPTist/src/api/types.ts @@ -5,4 +5,20 @@ export interface Result { data?: any rows?: Array, total?: number +} + +/** 课程信息 */ +export interface Classcourse { + id?: number|string, // 课程id + coursetitle?: string, // 课程名称 + coursetype?: string, // 课程类型 + courseverid?: string, // 课程版本id + coursedesc?: string, // 课程描述 + status?: number, // 课程状态 + teacherid?: number|string, // 教师id + entpcoursefileid?: number|string, // 课程文件id + classid?: number|string, // 班级id + entpcourseid?: number|string, // 章节中间表id + plandate?: string, // 计划时间 + opendate?: string, // 开课时间 } \ No newline at end of file diff --git a/src/renderer/src/AixPPTist/src/hooks/useScreening.ts b/src/renderer/src/AixPPTist/src/hooks/useScreening.ts index dd7d66b..e8cc32a 100644 --- a/src/renderer/src/AixPPTist/src/hooks/useScreening.ts +++ b/src/renderer/src/AixPPTist/src/hooks/useScreening.ts @@ -1,9 +1,10 @@ -import { useScreenStore, useSlidesStore } from '../store' +import { useScreenStore, useSlidesStore, useClasscourseStore } from '../store' import { enterFullscreen, exitFullscreen, isFullscreen } from '../utils/fullscreen' export default () => { const screenStore = useScreenStore() const slidesStore = useSlidesStore() + const classcourseStore = useClasscourseStore() // 课堂信息 // 进入放映状态(从当前页开始) const enterScreening = () => { @@ -19,7 +20,11 @@ export default () => { // 退出放映状态 const exitScreening = () => { - screenStore.setScreening(false) + const classcourse = classcourseStore.classcourse + if (!!classcourse) { //DOTO 有课堂,执行退相关操作 + console.log('退出放映状态') + window.close() + } else screenStore.setScreening(false) if (isFullscreen()) exitFullscreen() } diff --git a/src/renderer/src/AixPPTist/src/store/classcourse.ts b/src/renderer/src/AixPPTist/src/store/classcourse.ts new file mode 100644 index 0000000..7abd3fe --- /dev/null +++ b/src/renderer/src/AixPPTist/src/store/classcourse.ts @@ -0,0 +1,18 @@ +import { defineStore } from 'pinia' +import type { Classcourse } from '../api/types' + +export interface ClasscourseState { + classcourse: Classcourse +} + +export const useClasscourseStore = defineStore('classcourse', { + state: (): ClasscourseState => ({ + classcourse: null, // 课堂信息 + }), + + actions: { + setClasscourse(classcourse: Classcourse) { + this.classcourse = classcourse + }, + }, +}) \ No newline at end of file diff --git a/src/renderer/src/AixPPTist/src/store/index.ts b/src/renderer/src/AixPPTist/src/store/index.ts index 1ed4f2f..fe0cf22 100644 --- a/src/renderer/src/AixPPTist/src/store/index.ts +++ b/src/renderer/src/AixPPTist/src/store/index.ts @@ -3,6 +3,7 @@ import { useSlidesStore } from './slides' import { useSnapshotStore } from './snapshot' import { useKeyboardStore } from './keyboard' import { useScreenStore } from './screen' +import { useClasscourseStore } from './classcourse' export { useMainStore, @@ -10,4 +11,5 @@ export { useSnapshotStore, useKeyboardStore, useScreenStore, + useClasscourseStore, } \ No newline at end of file diff --git a/src/renderer/src/plugins/socket/index.js b/src/renderer/src/plugins/socket/index.js new file mode 100644 index 0000000..968ac53 --- /dev/null +++ b/src/renderer/src/plugins/socket/index.js @@ -0,0 +1,164 @@ +/** + * websocket 工具类(im 自己实现) +* 单例模式: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 +* 实现的方法为先判断实例存在与否,如果存在则直接返回,不存在就创建了再返回,这就确保了一个类只有一个实例对象。 +*/ +import useUserStore from '@/store/modules/user' // 用户信息 + +export class ChatWs { + instance = null; // 实例 + id = null; // 群聊id || 单聊id-用户id(userId) + closed = false; // 关闭状态 + onmessage = null; // 自定义处理 + errCount = 5; // 重连次数 (ms) 暂时不使用 + errTime = null; // 重连时间 (ms) 1秒内zhi间内不重连 + // 类型定义 + TYPES = { + group: 'group', // 群发 + single: 'single', // 单发 + beat: 'heart_beat', // 心跳 + } + static base = 'wss://file.ysaix.com:7868' + constructor() { + if (!ChatWs.instance) { + const userStore = useUserStore() // 用户信息 + const wsBase = import.meta.env.VITE_APP_WS_URL; // ws地址 + const url = `${wsBase||ChatWs.base}/ws/websocket/${userStore.id}`; + this.init(url); + ChatWs.instance = this; + } + return ChatWs.instance; + } + + // 初始化 + init(url) { + this.url = url; + this.ws = null; + const _this = this + this.heartCheck = { + timeout: 1000 * 10, // 60s + timeoutObj: null, + serverTimeoutObj: null, + reset() { + clearTimeout(this.timeoutObj); + clearTimeout(this.serverTimeoutObj); + return this; + }, + start() { + const self = this; + this.timeoutObj = setTimeout(function () { + // 这里发送一个心跳,后端收到后,返回一个心跳消息, + // onmessage拿到返回的心跳就说明连接正常 + console.log("websocket-发送心跳") + _this.sendMsgBeat(); + self.serverTimeoutObj = setTimeout(function () { + console.log("websocket-心跳响应超时") + // 如果超过一定时间还没重置,说明后端主动断开了 + _this.ws.close(); // 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次 + }, self.timeout); + }, this.timeout); + }, + }; + this.reconnect(); + } + // 重连 + reconnect() { + const self = this; + if (!!this.ws) { // 关闭之前的链接 + this.ws.close() + this.ws = null + } + this.ws = new WebSocket(this.url); + this.ws.onopen = function () { + console.log("websocket-连接成功") + self.heartCheck.reset().start(); + }; + this.ws.onmessage = function (e) { + // console.log("websocket-收到消息", e) + // 拿到任何消息都说明当前连接是正常的 + const isBeat = e.data == 'pong' + isBeat && self.heartCheck.reset().start(); + const exts = ['sessionId', 'pong'] // 不处理的消息头 + const isEmpty = !e.data + const isExts = exts.some(item => e.data.includes(item)) + if (isEmpty && isExts) return; + // 自定义处理 + self.onmessage && self.onmessage(e.data, e); + }; + this.ws.onerror = function (e) { + console.log("websocket-连接异常", e) + self.connectSocket() // 重连 + }; + this.ws.onclose = function (e) { + console.log("websocket-连接断开", e) + self.connectSocket() // 重连 + }; + } + connectSocket() { + this.heartCheck.reset() // 重置心跳 + if (self.closed) return; // 关闭状态不重连 + // if(self.errCount <= 0) return; // 超过重连次数 + // self.errCount--; // 重连次数减1 + if (this.errTime) { + const nowTime = Date.now(); + const bool = nowTime - this.errTime < 1000 // 1s内zhi间内不重连 + if (bool) return; // 1s内不重连 + } + this.errTime = Date.now(); + // 延时5s 后重连 + console.log('重连中...') + this.sleep(5000).then(_ => {this.reconnect()}) + } + // 发送消息 + send(msg) { + if (!msg) throw new Error("msg is not null") + if (!this.ws) throw new Error("ws is not null") + if (typeof msg === "object") msg = JSON.stringify(msg) + if (!msg.includes('"msg":')) throw new Error("msg 格式错误请重试") + this.ws.send(msg) + } + // 发送消息-带消息头(key) + sendMsg(head, content, option = {}) { + if (!head) throw new Error("head is not null") + if (!content) throw new Error("content is not null") + let msg = { head, content, ...option } + // 发送消息 + this.send(this.getMsgObj(msg)) + } + // 发送心跳 + sendMsgBeat() { + // this.send(this.getMsgObj('ping', this.TYPES.beat)) + this.ws.send('ping') + } + /** + * @description 获取消息对象 + * @param {*} msg 消息内容 + * @param {*} chatType 群发 group| 单发 single| 心态 heart_beat + * @param {*} id 群聊id || 单聊id-用户id(userId) + */ + getMsgObj(msg, chatType = 'group', id) { + if (typeof msg === "object") msg = JSON.stringify(msg) + const res = {msg, chatType} + // if (!id) throw new Error(`${type=='group'?'群ID':'用户ID'} is not null`) + if (chatType == 'group') res.groupId = id || this.id || '' + else if (chatType == 'single') res.to = id || this.id || '' + return res + } + // 监听 + watch(callback) { + callback && (this.onmessage = callback); + } + // 关闭链接 + close() { + this.closed = true; + this.ws.close(); + } + // 延时 ms 毫秒 + sleep(ms){ + return new Promise(resolve => setTimeout(resolve, ms)) + } +} +// 连接socket +export const connect = () => new ChatWs() +// 默认实例 +export default new ChatWs() \ No newline at end of file diff --git a/src/renderer/src/views/model/index.vue b/src/renderer/src/views/model/index.vue index 6085346..e387797 100644 --- a/src/renderer/src/views/model/index.vue +++ b/src/renderer/src/views/model/index.vue @@ -61,7 +61,9 @@ import { sessionStore } from '@/utils/store' // 学科名字文生图 // 组件引入 import ChooseTextbook from '@/components/choose-textbook/index.vue' import { menusEvent } from '@/plugins/vue3-menus' // 右键菜单 - +import ChatWs from '@/plugins/socket' +console.log('xxxx',ChatWs) +ChatWs.watch((data,e) => console.log('ws', data, e)) const router = useRouter() const userStore = useUserStore() // 用户信息 @@ -197,6 +199,19 @@ const getResourceList = async () => { // 统一HTTP处理 const HTTP_SERVER_API = (type, params = {}) => { switch (type) { + case 'addSmarttalk': { // 获取课程 + const def = { + fileId: '', // 文件id - Entpcoursefile 对应id + fileFlag: 'aptist', + fileShowName: courseObj.coursetitle + '.aptist', + textbookId: courseObj.textbookId, + levelFirstId: courseObj.levelFirstId, + levelSecondId: courseObj.levelSecondId, + fileSource: '个人', + fileRoot: '备课' + } + return API_smarttalk.creatAPT({...def, ...params}) + } case 'addEntpcourse': { // 添加课程 const node = courseObj.node || {} if (!node) return msgUtils.msgWarning('请选择章节?') @@ -279,6 +294,8 @@ const handleAll = async(type, row) =>{ } // 生成ppt课件-子级(slide) await HTTP_SERVER_API('addEntpcoursefile', params) + // 生成备课资源-Smarttalk + await HTTP_SERVER_API('addSmarttalk',{fileId: id}) // 刷新资源列表 await getResourceList() } else { diff --git a/src/renderer/src/views/prepare/container/class-start.vue b/src/renderer/src/views/prepare/container/class-start.vue index 3829064..af1e75a 100644 --- a/src/renderer/src/views/prepare/container/class-start.vue +++ b/src/renderer/src/views/prepare/container/class-start.vue @@ -78,7 +78,7 @@ - +