This commit is contained in:
小杨 2024-12-09 17:25:30 +08:00
commit 23e59531ea
18 changed files with 310 additions and 27 deletions

View File

@ -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/' 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' VITE_SHOW_DEV_TOOLS = 'true'

View File

@ -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/' 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' VITE_SHOW_DEV_TOOLS = 'false'

View File

@ -28,6 +28,7 @@ import * as API_entpcoursefile from '@/api/education/entpcoursefile' // 相关ap
import { PPTApi } from './api' import { PPTApi } from './api'
import { sessionStore } from '@/utils/store' // electron-store import { sessionStore } from '@/utils/store' // electron-store
import './api/watcher' // import './api/watcher' //
import './api/classcourse' //
const loading = ref(true) const loading = ref(true)
const _isPC = isPC() const _isPC = isPC()

View File

@ -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)

View File

@ -136,7 +136,7 @@ export class PPTApi {
id: currentSlide.id, id: currentSlide.id,
datacontent: JSON.stringify(currentSlide), datacontent: JSON.stringify(currentSlide),
} }
Utils.mxThrottle(() => {this.updateSlide(params)}, 1000, 2) Utils.mxThrottle(() => {this.updateSlide(params)}, 200, 2)
} }
} }
// 更新幻灯片 // 更新幻灯片

View File

@ -5,4 +5,20 @@ export interface Result {
data?: any data?: any
rows?: Array<any>, rows?: Array<any>,
total?: number 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, // 开课时间
} }

View File

@ -1,9 +1,10 @@
import { useScreenStore, useSlidesStore } from '../store' import { useScreenStore, useSlidesStore, useClasscourseStore } from '../store'
import { enterFullscreen, exitFullscreen, isFullscreen } from '../utils/fullscreen' import { enterFullscreen, exitFullscreen, isFullscreen } from '../utils/fullscreen'
export default () => { export default () => {
const screenStore = useScreenStore() const screenStore = useScreenStore()
const slidesStore = useSlidesStore() const slidesStore = useSlidesStore()
const classcourseStore = useClasscourseStore() // 课堂信息
// 进入放映状态(从当前页开始) // 进入放映状态(从当前页开始)
const enterScreening = () => { const enterScreening = () => {
@ -19,7 +20,11 @@ export default () => {
// 退出放映状态 // 退出放映状态
const exitScreening = () => { const exitScreening = () => {
screenStore.setScreening(false) const classcourse = classcourseStore.classcourse
if (!!classcourse) { //DOTO 有课堂,执行退相关操作
console.log('退出放映状态')
window.close()
} else screenStore.setScreening(false)
if (isFullscreen()) exitFullscreen() if (isFullscreen()) exitFullscreen()
} }

View File

@ -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
},
},
})

View File

@ -3,6 +3,7 @@ import { useSlidesStore } from './slides'
import { useSnapshotStore } from './snapshot' import { useSnapshotStore } from './snapshot'
import { useKeyboardStore } from './keyboard' import { useKeyboardStore } from './keyboard'
import { useScreenStore } from './screen' import { useScreenStore } from './screen'
import { useClasscourseStore } from './classcourse'
export { export {
useMainStore, useMainStore,
@ -10,4 +11,5 @@ export {
useSnapshotStore, useSnapshotStore,
useKeyboardStore, useKeyboardStore,
useScreenStore, useScreenStore,
useClasscourseStore,
} }

View File

@ -162,6 +162,10 @@ const setDialogForExport = (type: DialogForExportTypes) => {
.icon { .icon {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
:deep(svg) {
display: block !important;
}
} }
&:hover { &:hover {

View File

@ -396,12 +396,17 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
.icon { .icon {
margin-right: 3px; margin-right: 3px;
font-size: 14px; font-size: 14px;
:deep(svg) {
display: block !important;
}
} }
} }
.thumbnail-list { .thumbnail-list {
padding: 5px 0; padding: 5px 0;
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-bottom: 1px solid $borderColor;
} }
.thumbnail-item { .thumbnail-item {
display: flex; display: flex;
@ -480,7 +485,6 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
.page-number { .page-number {
height: 40px; height: 40px;
font-size: 12px; font-size: 12px;
border-top: 1px solid $borderColor;
line-height: 40px; line-height: 40px;
text-align: center; text-align: center;
color: #666; color: #666;

View File

@ -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()

View File

@ -208,6 +208,10 @@ export const createWindow = async (type, data) => {
autoHideMenuBar: true, autoHideMenuBar: true,
maximizable: false, maximizable: false,
} }
// pptlist的时候可以选择最大化
if (data.url == '/pptist'){
defOption.maximizable = true;
}
data.isConsole = true // 是否开启控制台 data.isConsole = true // 是否开启控制台
data.option = {...defOption, ...option} data.option = {...defOption, ...option}
const win = await toolWindow(type, data) const win = await toolWindow(type, data)

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="page"> <div class="page">
<!-- 习题筛选1 --> <!-- 习题筛选1 -->
<el-row style="width: 100%; height: 50px;"> <el-row style="width: 100%; height: 50px;">
<el-col :span="7"> <el-col :span="7">
@ -60,7 +60,7 @@
<div class="page-table" > <div class="page-table" >
<el-table <el-table
:data="workResource.entpCourseWorkList" :data="workResource.entpCourseWorkList"
style="width: 100%; height: calc(100% - 55px);" style="width: 100%; height: calc(100% - 50px);"
v-loading="pageParams.loading" v-loading="pageParams.loading"
> >
<el-table-column type="index" width="60" /> <el-table-column type="index" width="60" />
@ -89,13 +89,13 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页--> <!-- 分页-->
<div style="height: 55px;"> <div style="height: 50px;">
<el-pagination <el-pagination
v-show="pageParams.total > 0" v-show="pageParams.total > 0"
v-model:page="paginationParams.pageNum" v-model:page="paginationParams.pageNum"
v-model:limit="paginationParams.pageSize" v-model:limit="paginationParams.pageSize"
:total="pageParams.total" :total="pageParams.total"
:style="{ position: 'relative', 'margin-top': '5px' }" :style="{ position: 'relative', 'padding-top': '10px' }"
@change="getPaginationList" /> @change="getPaginationList" />
</div> </div>
</div> </div>
@ -188,6 +188,7 @@ const workResource = reactive({
}); // }); //
onMounted(() => { onMounted(() => {
console.log('entpCourseWorkTypeList', entpCourseWorkTypeList);
debounceQueryData(); // debounceQueryData(); //
}) })

View File

@ -61,7 +61,6 @@ import { sessionStore } from '@/utils/store' // 学科名字文生图
// //
import ChooseTextbook from '@/components/choose-textbook/index.vue' import ChooseTextbook from '@/components/choose-textbook/index.vue'
import { menusEvent } from '@/plugins/vue3-menus' // import { menusEvent } from '@/plugins/vue3-menus' //
const router = useRouter() const router = useRouter()
const userStore = useUserStore() // const userStore = useUserStore() //
@ -197,6 +196,19 @@ const getResourceList = async () => {
// HTTP // HTTP
const HTTP_SERVER_API = (type, params = {}) => { const HTTP_SERVER_API = (type, params = {}) => {
switch (type) { 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': { // case 'addEntpcourse': { //
const node = courseObj.node || {} const node = courseObj.node || {}
if (!node) return msgUtils.msgWarning('请选择章节?') if (!node) return msgUtils.msgWarning('请选择章节?')
@ -279,6 +291,8 @@ const handleAll = async(type, row) =>{
} }
// ppt-(slide) // ppt-(slide)
await HTTP_SERVER_API('addEntpcoursefile', params) await HTTP_SERVER_API('addEntpcoursefile', params)
// -Smarttalk
await HTTP_SERVER_API('addSmarttalk',{fileId: id})
// //
await getResourceList() await getResourceList()
} else { } else {

View File

@ -78,7 +78,7 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- im-chat 聊天组件 --> <!-- im-chat 聊天组件 -->
<im-chat ref="imChatRef" v-if="visible" @change="chatChange" /> <!-- <im-chat ref="imChatRef" v-if="visible" @change="chatChange" /> -->
</template> </template>
<script setup> <script setup>
@ -90,7 +90,7 @@ import vueQr from 'vue-qr/src/packages/vue-qr.vue' // 插件: 二维码
import imChat from '@/views/tool/components/imChat.vue' // im-chat- import imChat from '@/views/tool/components/imChat.vue' // im-chat-
import MsgEnum from '@/plugins/imChat/msgEnum' // -(nuem) import MsgEnum from '@/plugins/imChat/msgEnum' // -(nuem)
import * as commUtil from '@/utils/comm' // - import * as commUtil from '@/utils/comm' // -
import { toLinkWeb, getStaticUrl } from '@/utils/tool' // - import { toLinkWeb, createWindow, getStaticUrl, sessionStore } from '@/utils/tool' // -
import * as Http_ClassManage from '@/api/classManage' // api import * as Http_ClassManage from '@/api/classManage' // api
import * as Http_Classcourse from '@/api/teaching/classcourse' // api import * as Http_Classcourse from '@/api/teaching/classcourse' // api
@ -103,7 +103,7 @@ let baseUrl = import.meta.env.VITE_APP_BUILD_BASE_PATH
const userStore = useUserStore() const userStore = useUserStore()
const visible = ref(false) // const visible = ref(false) //
const myClassActive = ref({}) // APT const myClassActive = ref({}) // APT
const imChatRef = ref(null) // im-chat ref // const imChatRef = ref(null) // im-chat ref
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
const classForm = reactive({ // () const classForm = reactive({ // ()
form: {}, itemOption: [], option: {} form: {}, itemOption: [], option: {}
@ -177,7 +177,7 @@ const initData = () => {
teacherForm.itemOption = [ teacherForm.itemOption = [
// { label: '', prop: 'classid' }, // { label: '', prop: 'classid' },
// { label: '', prop: 'classcourseid' }, // { label: '', prop: 'classcourseid' },
{ label: '老师扫码', prop: 'qrUrl', show: false }, // { label: '', prop: 'qrUrl', show: false },
{ label: '手机登录', prop: 'mobile', show: false }, { label: '手机登录', prop: 'mobile', show: false },
{ label: '故障备用', prop: 'backup', show: false }, { label: '故障备用', prop: 'backup', show: false },
] ]
@ -257,18 +257,22 @@ const createClasscourse = async () => {
entpcourseid, evalid, coursetitle, entpcourseid, evalid, coursetitle,
plandate: curDate, opendate: curDate plandate: curDate, opendate: curDate
} }
// teacherForm.form.classcourseid = 100
teacherForm.form.classcourseid = await Http_Classcourse.addClasscourseReturnId(params) teacherForm.form.classcourseid = await Http_Classcourse.addClasscourseReturnId(params)
dt.loading = false dt.loading = false
// getClasscourseList('update') // // getClasscourseList('update') //
ElMessage.success('创建课程-成功') let msgEl = ElMessage.success('创建课程-成功')
// -pptList // -pptList
if (myClassActive.value.filetype == 'aippt') { if (myClassActive.value.filetype == 'aippt') {
const msgEl = ElMessage.warning({message:'正在打开公屏,请稍后...',duration: 0})
setTimeout(() => { setTimeout(() => {
msgEl.close() msgEl.close()
const classcourse = {...params, id: teacherForm.form.classcourseid} msgEl = ElMessage.warning({message:'正在打开公屏,请稍后...',duration: 0})
openPublicScreen(classcourse) setTimeout(() => {
}, 1500); msgEl.close()
const classcourse = {...params, id: teacherForm.form.classcourseid}
openPublicScreen(classcourse)
}, 2000);
}, 1000);
} }
} }
// //
@ -296,14 +300,23 @@ const removeClasscourse = async () => {
const classTeachingStart = async () => { const classTeachingStart = async () => {
const { classcourseid:id } = teacherForm.form const { classcourseid:id } = teacherForm.form
if (id) { // if (id) { //
const url = `/teaching/classteaching?classcourseid=${id}&actor=classTeachingOnPublicScreen` // -pptList
toLinkWeb(url) // web- if (myClassActive.value.filetype == 'aptist') {
visible.value = false // const msgEl = ElMessage.warning({message:'正在打开公屏,请稍后...',duration: 0})
handleClose() // im-chat setTimeout(() => {
msgEl.close()
openPublicScreen({id})
}, 2000);
}else {
const url = `/teaching/classteaching?classcourseid=${id}&actor=classTeachingOnPublicScreen`
toLinkWeb(url) // web-
visible.value = false //
}
} }
} }
// //
const getQrUrl = async() => { const getQrUrl = async() => {
// console.log('')
const { classcourseid:id } = teacherForm.form const { classcourseid:id } = teacherForm.form
const { userName, userId } = userStore.user const { userName, userId } = userStore.user
if (!id||!userName) return if (!id||!userName) return
@ -336,6 +349,7 @@ const getQrUrl = async() => {
// //
const openPublicScreen = (classcourse) => { const openPublicScreen = (classcourse) => {
console.log('打开公屏', classcourse)
const resource = toRaw(myClassActive.value) const resource = toRaw(myClassActive.value)
sessionStore.set('curr.resource', resource) // sessionStore.set('curr.resource', resource) //
sessionStore.set('curr.classcourse', classcourse) // sessionStore.set('curr.classcourse', classcourse) //
@ -346,6 +360,7 @@ const openPublicScreen = (classcourse) => {
sessionStore.set('curr.classcourse', null) // sessionStore.set('curr.classcourse', null) //
} }
}) })
visible.value = false //
} }
// //
@ -386,12 +401,14 @@ watch(() => classForm.form.classid, (val)=> {
// -id // -id
watch(() => teacherForm.form.classcourseid, (val) => { watch(() => teacherForm.form.classcourseid, (val) => {
const bool = !!val const bool = !!val
const isApt = myClassActive.filetype=='apt'
// - // -
bool && getQrUrl() isApt && bool && getQrUrl()
// id // id
teacherForm.itemOption.forEach(o => { teacherForm.itemOption.forEach(o => {
// id // id
if (['qrUrl','backup'].includes(o.prop)) o.show = bool const arr = isApt ? ['qrUrl','backup'] : ['backup']
if (arr.includes(o.prop)) o.show = bool
// id // id
if (['mobile'].includes(o.prop)) o.show = !bool if (['mobile'].includes(o.prop)) o.show = !bool
}) })

View File

@ -192,8 +192,11 @@ import ClassReserv from '@/views/classManage/classReserv.vue'
import TreeLog from '@/views/prepare/components/treeLog.vue' import TreeLog from '@/views/prepare/components/treeLog.vue'
import classStart from './container/class-start.vue' // import classStart from './container/class-start.vue' //
import MsgEnum from '@/plugins/imChat/msgEnum' // im import MsgEnum from '@/plugins/imChat/msgEnum' // im
import Chat from '@/utils/chat' // im // import Chat from '@/utils/chat' // im
if (!Chat.imChat) Chat.init() // if (!Chat.imChat) Chat.init()
// import ChatWs from '@/plugins/socket'
// console.log('xxxx',ChatWs)
// ChatWs.watch((data,e) => console.log('ws', data, e))
const toolStore = useToolState() const toolStore = useToolState()
const fs = require('fs') const fs = require('fs')

View File

@ -34,7 +34,6 @@ import {getDept } from '@/api/login'
import { listEvaluation } from '@/api/subject/index' import { listEvaluation } from '@/api/subject/index'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import {ElMessage} from 'element-plus' import {ElMessage} from 'element-plus'
import { clearBookInfo } from '@/utils/ruoyi'
import { sessionStore } from '@/utils/store' import { sessionStore } from '@/utils/store'
import {listClassmain} from '@/api/classManage/index' import {listClassmain} from '@/api/classManage/index'
// //