Compare commits

...

14 Commits

23 changed files with 536 additions and 149 deletions

View File

@ -92,6 +92,8 @@
"tinycolor2": "^1.6.0",
"tinymce": "6.8.3",
"tippy.js": "^6.3.7",
"v-viewer": "^3.0.11",
"viewerjs": "^1.11.7",
"vite-plugin-electron": "^0.28.8",
"vue": "^3.4.34",
"vue-cropper": "1.0.3",

View File

@ -4,21 +4,51 @@
import ChatWs from '@/plugins/socket' // 聊天socket
import { sessionStore } from '@/utils/store' // electron-store 状态管理
import { useClasscourseStore } from '../store'
import * as API_classcourse from '@/api/teaching/classcourse' // 后端api
import { MsgEnum } from './types'
// import msgUtils from '@/plugins/modal' // 消息工具
export default () => {
const classcourse = sessionStore.get('curr.classcourse') // 课堂信息
const courseId = classcourse?.id // 课堂id
const timgroupid = classcourse?.timgroupid // 群组id
const classcourseStore = useClasscourseStore() // 课堂信息-状态管理
if (!ChatWs.ws) ChatWs.init()
// 开课消息
const startCourse = async() => {
// await API_classcourse.updateClasscourse({ id: classcourse.id, status: 'open' })
ChatWs.sendMsg('open', {id: courseId})
return Promise.resolve()
}
// 下课消息
const exitCourse = async() => {
if(!timgroupid) throw new Error('未获取到群组ID')
await API_classcourse.updateClasscourse({ id: classcourse.id, status: 'closed' })
await API_classcourse.updateClasscourse({ id: courseId, status: 'closed' })
return ChatWs.closedCourse(timgroupid)
}
// 翻页消息
const slideFlapping = (msg:object) => {
return new Promise(async (resolve, reject) => {
const isWs = !!ChatWs.ws && ChatWs.ws.readyState === 1 // 是否有socket连接
if(!timgroupid) return reject('未获取到群组ID')
else if(!isWs) return reject('信异常,请重试!')
const {current: paging, animation: cartoonTimes} = msg || {}
const head = MsgEnum.HEADS.MSG_slideFlapping
ChatWs.sendMsg(head, msg) // 发送消息
API_classcourse.setPaging({ id: courseId, paging, cartoonTimes})
// 更新本地缓存
sessionStore.set('curr.classcourse.paging', paging)
sessionStore.set('curr.classcourse.cartoonTimes', cartoonTimes)
classcourseStore.classcourse.paging = paging
classcourseStore.classcourse.cartoonTimes = cartoonTimes
return resolve(true)
})
}
return {
exitCourse,
classcourse,
groupid: timgroupid,
classcourse,
exitCourse,
slideFlapping,
}
}

View File

@ -14,6 +14,7 @@ const slidesStore = useStore.useSlidesStore() // 幻灯片-状态管理
const screenStore = useStore.useScreenStore() // 全屏-状态管理
const classcourseStore = useStore.useClasscourseStore() // 课堂信息-状态管理
const classcourse = sessionStore.get('curr.classcourse') // 课堂信息
const isPublic = sessionStore.get('curr.isPublic') // 是否公屏开课
export class Classcourse {
msgObj:ElMessageBox = null // 提示消息对象
@ -36,26 +37,22 @@ export class Classcourse {
// 如果课堂信息有值则连接socket
if (isCourse) {
// 连接socket
if (!ChatWs.ws) ChatWs.init()
ChatWs.id = classcourse.timgroupid // 群组id
if (!ChatWs.ws) {
ChatWs.init().then(_ => {
isPublic && ChatWs.sendMsg('open', {id: classcourse.id})
// isPublic && console.log('socket-开课消息-已发送')
})
}
this.classcourse = classcourse // 课堂信息
this.id = classcourse.id // 课堂id
// 如果课堂信息有paging则更新当前页码
const { paging } = classcourse
const { paging, cartoonTimes } = classcourse
const isPaging = !!paging || paging === 0
if (isPaging) {
await this.sleep(200)
emitter.emit('useExecPlay', {key:'turnSlideToIndex', paging})
await this.sleep(1000)
// 如果课堂信息有paging则更新动画播放状态
const isAnim = !!classcourse.cartoonTimes
if (isAnim) { // 动画播放
for (let i = 0; i < classcourse.cartoonTimes; i++) {
// 异步执行动画
emitter.emit('useExecPlay', {key:'execNext', isAsync:true})
}
}
}
const isAnim = !!cartoonTimes || cartoonTimes === 0
if (isPaging) slidesStore.updateSlideIndex(paging)
if (isAnim) slidesStore.updateAnimationIndex(cartoonTimes)
// 课堂信息-状态管理
classcourseStore.setClasscourse(classcourse)
// 待上课提示

View File

@ -124,6 +124,8 @@ export class MsgEnum {
MSG_classlecturePagesrc : 'classlecturePagesrc',
/** @desc: 课堂作业|活动 */
MSG_homework : 'HOMEWORK',
/** @desc: 公屏 - 课堂作业|活动 */
MSG_pushSreen_work : 'pushSreen-work',
/** @desc: 点赞 */
MSG_dz : 'dz',
/** @desc: 疑惑 */

View File

@ -0,0 +1,33 @@
/**
* -
*/
export default class Upvote {
instance: any = null // 自身实例
upvoteRef: any = null // 点赞组件
constructor(elRef?: any) {
if(!!elRef) this.upvoteRef = elRef // 点赞组件
if (!Upvote.Instance) {
Upvote.Instance = this
}
return Upvote.Instance
}
// 初始化
init(elRef) {
if(!!elRef) this.upvoteRef = elRef // 点赞组件
return this
}
// 打开点赞或者疑问 1点赞 2疑问
trigger(type) {
this.upvoteRef?.value?.trigger?.(type)
return this
}
// 静态方法-初始化
static init(elRef) {
return new Upvote(elRef)
}
// 静态方法-打开点赞或者疑问 1点赞 2疑问
static trigger(type) {
return new Upvote().trigger(type)
}
}

View File

@ -11,8 +11,9 @@ import ChatWs from '@/plugins/socket' // 聊天socket
import Classcourse from './classcourse' // 课程相关
import msgUtils from '@/plugins/modal' // 消息工具
import { Homework } from './index' // api-作业相关
import emitter from '@/utils/mitt' //mitt 事件总线
// import emitter from '@/utils/mitt' //mitt 事件总线
import useExecPlay from '../views/Screen/hooks/useExecPlay' // 播放控制
import hooksUpvote from './upvote' // 点赞-工具
/**
* @description
@ -22,7 +23,7 @@ export default () => {
const classcourseStore = store.useClasscourseStore() // 课堂信息-状态管理
const resource = sessionStore.get('curr.resource') // apt 资源
const smarttalk = sessionStore.get('curr.smarttalk') // 备课资源
const execPlay = useExecPlay() // 播放控制
const { execNext, turnPrevSlide } = useExecPlay(false) // 不加载钩子
// 监听幻灯片内容变化
watch(() => slidesStore.slides, (newVal, oldVal) => {
PPTApi.updateSlides(newVal, oldVal) // 更新幻灯片内容
@ -98,23 +99,23 @@ export default () => {
case MsgEnum.HEADS.MSG_slideFlapping: // 幻灯片翻页
const slideIndex = content?.current || 0
const type = content?.animation
// if (type === 'Nextsteps') emitter.emit('useExecPlay', 'execNext') // 下一步
if (type === 'Nextsteps') emitter.emit('useExecPlay', {key:'execNext', isAsync:true}) // 下一步
else if (type === 'Previoustep') emitter.emit('useExecPlay', 'turnPrevSlide') // 上一步清空-动画
if (type === 'Nextsteps') execNext(true) // 下一步-异步动画
else if (type === 'Previoustep') turnPrevSlide() // 上一步清空-动画
else slidesStore.updateSlideIndex(slideIndex) // 更新幻灯片下标
break
case MsgEnum.HEADS.MSG_homework: // 作业|活动-布置
if (!content.classWorkId) return
Homework.showHomework(content.classWorkId)
// case MsgEnum.HEADS.MSG_homework: // 作业|活动-布置 不处理
case MsgEnum.HEADS.MSG_pushSreen_work: // 打开-作业|活动
if (!content.id) return
Homework.showHomework(content.id)
break
case MsgEnum.HEADS.MSG_closed: // 下课:
close()
break
case MsgEnum.HEADS.MSG_dz: // 点赞
emitter.emit('upvoteTrigger', 1)
hooksUpvote.trigger(1)
break
case MsgEnum.HEADS.MSG_yh: // 疑惑
emitter.emit('upvoteTrigger', 2)
hooksUpvote.trigger(2)
break
case MsgEnum.HEADS.MSG_0010: // 备用
break

View File

@ -3,16 +3,21 @@ import type { Classcourse } from '../api/types'
export interface ClasscourseState {
classcourse: Classcourse | any, // 课堂信息
isEmit: boolean, // 是否加载监听事件(动画播放)
}
export const useClasscourseStore = defineStore('classcourse', {
state: (): ClasscourseState => ({
classcourse: null, // 课堂信息
isEmit: false, // 是否加载监听事件(动画播放)
}),
actions: {
setClasscourse(classcourse: Classcourse) {
this.classcourse = classcourse
},
setIsEmit(isEmit: boolean) {
this.isEmit = isEmit
},
},
})

View File

@ -33,7 +33,8 @@ export interface SlidesState {
slides: Slide[]
slideIndex: number
viewportSize: number
viewportRatio: number
viewportRatio: number,
animationIndex: number, // 不是从0开始
workList:Object[],
workItem:Object[],
}
@ -46,6 +47,7 @@ export const useSlidesStore = defineStore('slides', {
slideIndex: 0, // 当前页面索引
viewportSize: 1000, // 可视区域宽度基数
viewportRatio: 0.5625, // 可视区域比例默认16:9
animationIndex: 0, // 不是从0开始
workList:[],// 活动的列表
workItem:[],// 获取到的所有pptlist
}),
@ -206,6 +208,9 @@ export const useSlidesStore = defineStore('slides', {
updateSlideIndex(index: number) {
this.slideIndex = index
},
updateAnimationIndex(index: number) {
this.animationIndex = index
},
addElement(element: PPTElement | PPTElement[]) {
const elements = Array.isArray(element) ? element : [element]

View File

@ -30,14 +30,10 @@
@close="timerlVisible = false"
/>
<div class="tools-left">
<div class="tools-left" v-if="!classcourse">
<IconLeftTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execPrev()" />
<IconRightTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execNext()" />
</div>
<!-- 点赞组件 -->
<div style="z-index: 999;position: absolute;top:10px">
<upvote-vue ref="upvoteRef" type="2"></upvote-vue>
</div>
<div
class="tools-right" :class="{ 'visible': rightToolsVisible }"
@mouseleave="rightToolsVisible = false"
@ -59,7 +55,7 @@
</template>
<script lang="ts" setup>
import { ref , watchEffect} from 'vue'
import { ref , watchEffect, onMounted, onUnmounted} from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore ,useScreenStore, useClasscourseStore} from '../../store'
import type { ContextmenuItem } from '../../components/Contextmenu/types'
@ -73,18 +69,15 @@ import ScreenSlideList from './ScreenSlideList.vue'
import SlideThumbnails from './SlideThumbnails.vue'
import WritingBoardTool from './WritingBoardTool.vue'
import CountdownTimer from './CountdownTimer.vue'
import upvoteVue from '@/views/tool/components/upvote.vue' // -
import emitter from '@/utils/mitt';
import Chat from '../../api/chat' //
// import * as emits from './hooks/emitter'
// emits.init() //
const props = defineProps<{
changeViewMode: (mode: 'base' | 'presenter') => void
}>()
const { slides, slideIndex } = storeToRefs(useSlidesStore())
const { classcourse } = storeToRefs(useClasscourseStore()) //
const { classcourse, isEmit } = storeToRefs(useClasscourseStore()) //
const {
autoPlayTimer,
@ -105,26 +98,6 @@ const {
execNext,
animationIndex,
} = useExecPlay()
// zdg: 使
const execPlay = {
autoPlayTimer,
autoPlay,
closeAutoPlay,
autoPlayInterval,
setAutoPlayInterval,
loopPlay,
setLoopPlay,
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
execPrev,
execNext,
animationIndex,
}
const { slideWidth, slideHeight } = useSlideSize()
const { exitScreening } = useScreening()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
@ -135,7 +108,6 @@ const writingBoardToolVisible = ref(false)
const timerlVisible = ref(false)
const slideThumbnailModelVisible = ref(false)
const laserPen = ref(false)
const upvoteRef = ref(null)
const screenStore =useScreenStore()
const contextmenus = (): ContextmenuItem[] => {
return [
@ -226,25 +198,6 @@ const exitCourse = async () => {
exitScreening() //
}
// 1 2
emitter.on('upvoteTrigger', (type) => {
upvoteRef.value?.trigger(type)
});
//
emitter.on('useExecPlay', (data: string|any) => {
console.log('useExecPlay', data)
if (!data) throw new Error('参数错误')
if (typeof data === 'string') { //
if (execPlay[data]) execPlay[data]()
else throw new Error('方法不存在')
} else { //
const { key, ...params } = data || {}
const paramsArray = Object.values(params)
if (execPlay[key]) execPlay[key](...paramsArray)
else throw new Error('方法不存在')
}
})
</script>
<style lang="scss" scoped>

View File

@ -78,7 +78,7 @@
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore, useClasscourseStore } from '../../store'
import type { ContextmenuItem } from '../../components/Contextmenu/types'
@ -95,6 +95,7 @@ import ScreenSlideList from './ScreenSlideList.vue'
import WritingBoardTool from './WritingBoardTool.vue'
import CountdownTimer from './CountdownTimer.vue'
import Divider from '../../components/Divider.vue'
import emitter from '@/utils/mitt';
import Chat from '../../api/chat' //
const props = defineProps<{
@ -102,7 +103,7 @@ const props = defineProps<{
}>()
const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
const { classcourse } = storeToRefs(useClasscourseStore()) //
const { classcourse, isEmit } = storeToRefs(useClasscourseStore()) //
const slideListWrapRef = ref<HTMLElement>()
const thumbnailsRef = ref<HTMLElement>()
@ -120,7 +121,6 @@ const {
turnSlideToId,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
const { exitScreening } = useScreening()
const { slidesLoadLimit } = useLoadSlides()
@ -210,6 +210,7 @@ const contextmenus = (): ContextmenuItem[] => {
},
]
}
</script>
<style lang="scss" scoped>

View File

@ -5,14 +5,20 @@ import { useSlidesStore, useClasscourseStore } from '../../../store'
import { KEYS } from '../../../configs/hotkey'
import { ANIMATION_CLASS_PREFIX } from '../../../configs/animation'
import message from '../../../utils/message'
import emitter from '@/utils/mitt';
import Chat from '../../../api/chat' // 聊天封装
// import ChatWs from '@/plugins/socket' // 聊天socket
// import { MsgEnum } from '../../../api/types' // 消息枚举
export default () => {
export default (isLoader?: boolean = true) => {
// isLoader 是否执行 onMounted, onUnmounted
const chatApi = Chat()
const slidesStore = useSlidesStore()
const classcourseStore = useClasscourseStore() // 课堂信息-状态管理
const { slides, slideIndex, formatedAnimations } = storeToRefs(slidesStore)
const { slides, slideIndex, formatedAnimations, animationIndex } = storeToRefs(slidesStore)
// 当前页的元素动画执行到的位置
const animationIndex = ref(0)
// const animationIndex = ref(0)
// 动画执行状态
const inAnimation = ref(false)
@ -70,7 +76,7 @@ export default () => {
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
}
}
if (isLoader) { // 加载相关钩子
onMounted(() => {
const firstAnimations = formatedAnimations.value[0]
if (firstAnimations && firstAnimations.animations.length) {
@ -78,6 +84,7 @@ export default () => {
if (autoExecFirstAnimations) runAnimation()
}
})
}
// 撤销元素动画,除了将索引前移外,还需要清除动画状态
const revokeAnimation = () => {
@ -141,7 +148,6 @@ export default () => {
inAnimation.value = false
}
const execNext = (isAsync: boolean) => {
console.log('execNext', isAsync)
if (formatedAnimations.value.length && animationIndex.value < formatedAnimations.value.length) {
runAnimation(isAsync)
}
@ -177,8 +183,7 @@ export default () => {
// 鼠标滚动翻页
const mousewheelListener = (e: WheelEvent) => {
// console.log('mousewheel', e)
// 课堂信息存在时,不允许翻页
if (!!classcourseStore.classcourse) e.preventDefault()
e.preventDefault() // 阻止默认事件
mousewheelListenerThrottle(e)
}
const mousewheelListenerThrottle = throttle(function(e: WheelEvent) {
@ -209,12 +214,17 @@ export default () => {
}
}
// 向上翻页/向下翻页
const turning = (e, type) => {
const turning = async (e, type) => {
e.preventDefault() // 阻止默认事件
// 课堂信息存在时,不允许翻页
if (!!classcourseStore.classcourse) return
if (type === 'prev') execPrev()
else if (type === 'next') execNext()
if (classcourseStore.classcourse) { // 上课中
const current = slideIndex.value
const animation = animationIndex.value
const animationSteps = type == 'next'?'Nextsteps':'Previoustep'
const msg = { current, animation, animationSteps}
chatApi.slideFlapping(msg)
}
}
// 快捷键翻页
const keydownListener = (e: KeyboardEvent) => {
@ -229,9 +239,10 @@ export default () => {
key === KEYS.PAGEDOWN
) turning(e, 'next')
}
onMounted(() => document.addEventListener('keydown', keydownListener))
onUnmounted(() => document.removeEventListener('keydown', keydownListener))
if (isLoader) { // 加载相关钩子
onMounted(() => {document.addEventListener('keydown', keydownListener)})
onUnmounted(() => {document.removeEventListener('keydown', keydownListener)})
}
// 切换到上一张/上一张幻灯片(无视元素的入场动画)
const turnPrevSlide = () => {

View File

@ -2,6 +2,10 @@
<div class="pptist-screen">
<BaseView :changeViewMode="changeViewMode" v-if="viewMode === 'base'" />
<PresenterView :changeViewMode="changeViewMode" v-else-if="viewMode === 'presenter'" />
<!-- 点赞组件 -->
<upvote-vue ref="upvoteRef" type="2"></upvote-vue>
<!-- <div style="z-index: 999;position: absolute;top:10px">
</div> -->
</div>
</template>
@ -9,9 +13,11 @@
import { onMounted, onUnmounted, ref } from 'vue'
import { KEYS } from '../../configs/hotkey'
import useScreening from '../../hooks/useScreening'
import hooksUpvote from '../../api/upvote' // -
import BaseView from './BaseView.vue'
import PresenterView from './PresenterView.vue'
import upvoteVue from '@/views/tool/components/upvote.vue' // -
const viewMode = ref<'base' | 'presenter'>('base')
@ -20,6 +26,8 @@ const changeViewMode = (mode: 'base' | 'presenter') => {
}
const { exitScreening } = useScreening()
const upvoteRef = ref(null)
hooksUpvote.init(upvoteRef) //
// 退
const keydownListener = (e: KeyboardEvent) => {

View File

@ -95,3 +95,11 @@ export function getCourseTeachingMsg(id) {
})
}
export function setPaging(data) {
return request({
url: '/education/classcourse/record/paging',
method: 'post',
data
})
}

View File

@ -0,0 +1,172 @@
<template>
<draggable handle=".header-btn" :draggable="false" item-key="backgroundColor" v-model="gridPicList" class="grid-pic-wrap" :style="getGrid">
<template #item="{ element, index }">
<div class="grid-pic-item" :key="element.backgroundColor" :style="getWH(element,index)">
<div class="delete-btn" @click="gridPicList.splice(index,1)">X</div>
<div class="header-btn"></div>
<ViewerItem :gridPicList="gridPicList" :index="index" :images="[element.src]"></ViewerItem>
</div>
</template>
</draggable>
<el-input style="position:fixed;bottom: 20px;right: 80px;width: 1000px" v-model="inputValue" type="text" />
<el-button class="add-btn" @click="addPic">
添加
</el-button>
</template>
<script setup>
import {ref, computed} from 'vue'
import Draggable from 'vuedraggable'
import ViewerItem from "./viewer-item.vue";
const gridPicList = ref([])
const inputValue = ref('')
//
const getWH = (item,index)=>{
return {
backgroundColor: item.backgroundColor,
'grid-area': 'a' + index
}
}
// grid
const getGrid = computed(() => {
switch (gridPicList.value.length) {
case 1:
return {
'grid-template-areas':
`"a0"`
}
case 2:
return {
'grid-template-areas':
`"a0 a1"`
}
case 3:
return {
'grid-template-areas':
`"a0 a1"
"a0 a2"`
}
case 4:
return {
'grid-template-areas':
`"a0 a2"
"a1 a3"`
}
case 5:
return {
'grid-template-areas':
`"a0 a2 a4"
"a1 a3 a4"`
}
case 6:
return {
'grid-template-areas':
`"a0 a2 a4"
"a1 a3 a5"`
}
case 7:
return {
'grid-template-areas':
`"a0 a2 a4"
"a0 a2 a4"
"a0 a2 a5"
"a1 a3 a5"
"a1 a3 a6"
"a1 a3 a6"`
}
case 8:
return {
'grid-template-areas':
`"a0 a3 a6"
"a0 a3 a6"
"a1 a4 a6"
"a1 a4 a7"
"a2 a5 a7"
"a2 a5 a7"`
}
case 9:
return {
'grid-template-areas':
`"a0 a3 a6"
"a1 a4 a7"
"a2 a5 a8"`
}
default:
return {
width: '100%',
height: '100%'
}
}
})
//
const addPic = () => {
if (gridPicList.value.length >= 9) {
return
}
gridPicList.value.push({
src: inputValue.value,
backgroundColor: getRandomColor()
})
inputValue.value = ''
}
//
function getRandomColor() {
let r = Math.floor(Math.random() * 256).toString(16);
let g = Math.floor(Math.random() * 256).toString(16);
let b = Math.floor(Math.random() * 256).toString(16);
// 0
r = r.length === 1? '0' + r : r;
g = g.length === 1? '0' + g : g;
b = b.length === 1? '0' + b : b;
return `#${r}${g}${b}`;
}
</script>
<style scoped lang="scss">
.grid-pic-wrap{
width: 100%;
height: 100%;
display: grid;
overflow: hidden;
.grid-pic-item{
//animation: fadeIn 0.5s ease-in-out forwards;
background-color: #0a84ff;
position: relative;
.delete-btn{
position: absolute;
top: 0;
right: 10px;
z-index: 999;
&:hover{
color: #fff;
cursor: pointer;
}
}
.header-btn{
position: absolute;
z-index: 998;
height: 30px;
width: 100%;
border-bottom: 1px dotted #ccc;
}
}
}
.add-btn{
position: fixed;
right: 20px;
bottom: 20px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<viewer :ref="collectRef('viewerRef'+index)" :options="optins" :images="images" class="images clearfix">
<template #default="scope">
<img v-for="src in scope.images" :key="index" :src="src" style="display: none">
</template>
</viewer>
</template>
<script setup>
import {ref, watch, nextTick} from "vue";
const props = defineProps({
images: {
type: Object,
default: () => {}
},
index: {
type: Number,
default: 0
},
gridPicList: {
type: Array,
default: () => []
}
})
const refs = ref([]);
const collectRef = (key) => {
return (el) => {
refs.value[key] = el;
};
};
//viewer
const optins = {
"inline": true,
"button": false,
"navbar": false,
"title": false,
"toolbar": false,
"tooltip": true,
"movable": true,
"zoomable": true,
"rotatable": true,
"scalable": true,
"transition": true,
"fullscreen": true,
"keyboard": true
}
const initViewers = () => {
refs.value['viewerRef'+props.index]?.rebuildViewer()
}
watch(props.gridPicList, (newValue, oldValue) => {
nextTick(()=>{
initViewers()
})
});
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,26 @@
/**
* 无限滚动
*/
import { nextTick } from 'vue'
const mountedHook = async (el, binding) => {
console.log(el, binding)
const value = binding.value
if (typeof value !== 'function') return console.error('v-scroll must be a function')
await nextTick()
}
export default {
// Hooks for Vue3
mounted(el, binding) {
mountedHook(el, binding)
},
// Hooks for Vue2
inserted(el, binding) {
mountedHook(el, binding)
},
update(el, binding){
},
updated(el, binding){
},
}

View File

@ -17,14 +17,14 @@ import log from 'electron-log/renderer' // 渲染进程日志-文件记录
import customComponent from '@/components/common' // 自定义组件
import plugins from './plugins' // plugins插件
import useUserStore from '@/store/modules/user'
import VueViewer from 'v-viewer'
import 'viewerjs/dist/viewer.css'
if(process.env.NODE_ENV != 'development') { // 非开发环境,将日志打印到日志文件
Object.assign(console, log.functions) // 渲染进程日志-控制台替换
}
const app = createApp(App)
//专为菁优网配置的请求转发
app.config.globalProperties.$requestGetJYW = (url,config)=>{
config.params = config.params?config.params:{}
@ -42,6 +42,7 @@ import Directive from '@/AixPPTist/src/plugins/directive'
app.use(router)
.use(store)
.use(VueViewer)
.use(ElementPlus, { locale: zhLocale })
.use(customComponent) // 自定义组件
.use(plugins)

View File

@ -98,6 +98,8 @@ export class MsgEnum {
MSG_classlecturePagesrc : 'classlecturePagesrc',
/** @desc: 课堂作业|活动 */
MSG_homework : 'HOMEWORK',
/** @desc: 公屏 - 课堂作业|活动 */
MSG_pushSreen_work : 'pushSreen-work',
/** @desc: 点赞 */
MSG_dz : 'dz',
/** @desc: 疑惑 */

View File

@ -31,6 +31,11 @@ export const constantRoutes = [
component: () => import('@/AixPPTist/src/App.vue'),
hidden: true
},
{
path: '/gridPic',
component: () => import('@/components/grid-pic/index.vue'),
hidden: true
},
{
path: '/model',
component: Layout,

View File

@ -3,16 +3,16 @@
<!-- <div class="class-reserv-tabs">
<el-segmented v-model="tabActive" block :options="tabOptions" size="large" />
</div>-->
<div class="class-reserv-body">
<div class="class-reserv-body" v-infinite-scroll="load">
<template v-for="(item, index) in dataList" :key="index">
<reserv-item
<!-- <reserv-item
:style="{'background-color': index%2==0?'#f5f5f5':''}"
:item="item"
v-if="item.bookImg"
@open-edit="reservDialog.openDialog(item)"
@delete-reserv="deleteReserv(item)"
@change="(...o) => emit('change', ...o)"
></reserv-item>
></reserv-item> -->
<reserv-item-apt
v-if="!item.bookImg"
:style="{'background-color': index%2==0?'#f5f5f5':''}"
@ -22,13 +22,14 @@
@change="(...o) => emit('change', ...o)"
></reserv-item-apt>
</template>
<el-divider v-if="page.isEnd">到底了没了</el-divider>
</div>
<reserv ref="reservDialog"></reserv>
</el-container>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, computed, watch, reactive } from 'vue'
import { getSelfReserv } from '@/api/classManage'
import { listClasscourseNew } from '@/api/teaching/classcourse' // api
import ReservItem from '@/views/classManage/reserv-item.vue'
@ -36,6 +37,7 @@ import Reserv from '@/views/prepare/container/reserv.vue'
import { useToolState } from '@/store/modules/tool'
import useUserStore from '@/store/modules/user'
import ReservItemApt from '@/views/classManage/reserv-item-apt.vue'
import vScroll from '@/directive/scroll' // --
// import Chat from '@/utils/chat' // im
// if (!Chat.imChat) Chat.init()
@ -44,6 +46,12 @@ const reservDialog = ref(null)
const tabOptions = ref(['进行中', '已结束'])
const tabActive = ref('进行中')
const dataList = ref([])
const page = reactive({
pageNum: 0, //
pageSize: 10, //
total: 0, //
isEnd: false //
})
const toolStore = useToolState()
const userStore = useUserStore()
@ -72,21 +80,42 @@ const deleteReserv = (item) => {
})*/
//
const getData = () => {
Promise.all([listClasscourseNew({teacherid: userStore.id,evalid: props.curNode.id,pageSize:1000}), getSelfReserv({ex2:props.curNode.id})]).then(([res1,res2])=>{
let list = res2.data || []
let list2 = res1.rows || []
// list.sort((a,b) => { if(a.status=='') return -1; else return 0 })
list = list.concat(list2)
const { pageNum, pageSize } = page
const params = {
evalid: props.curNode.id,
teacherid: userStore.id,
pageNum, pageSize
}
listClasscourseNew(params)
.then((res) => {
const list = res.rows || []
const total = res.total || 0
list.sort((a,b) => { return new Date(b.createTime) - new Date(a.createTime) })
dataList.value = list
dataList.value.push(...list)
page.total = total //
page.isEnd = dataList.value.length == total //
})
// aippt+ppt
// Promise.all([listClasscourseNew({teacherid: userStore.id,evalid: props.curNode.id,pageSize:1000}), getSelfReserv({ex2:props.curNode.id})]).then(([res1,res2])=>{
// let list = res2.data || []
// let list2 = res1.rows || []
// // list.sort((a,b) => { if(a.status=='') return -1; else return 0 })
// list = list.concat(list2)
// list.sort((a,b) => { return new Date(b.createTime) - new Date(a.createTime) })
// dataList.value = list
// })
/*getSelfReserv().then((res) => {
const list = res.data || []
list.sort((a,b) => { if(a.status=='上课中') return -1; else return 0 })
dataList.value = list
})*/
}
//
const load = () => {
if(page.isEnd) return console.log('已加载完-所有') //
page.pageNum++
getData()
}
watch(
() => [dataList,toolStore.isToolWin,props.curNode],
() => {
@ -96,13 +125,14 @@ watch(
}
)
onMounted(() => {
getData() //
// getData() //
})
</script>
<style scoped lang="scss">
.class-reserv-wrap {
height: 100%;
// height: 300px;
display: flex;
flex-direction: column;
//padding: 15px 10px;

View File

@ -61,7 +61,8 @@
<div>
<div v-if="myClassActive.filetype=='apt'">开始新的课堂需要点击先创建课堂才能显示手机二维码</div>
<div v-else>开始新的课堂需要点击先创建课堂</div>
<el-button type="warning" :loading="dt.loading" @click="createClasscourse">创建课堂</el-button>
<el-button type="warning" :loading="dt.loading" @click="createClasscourse()">创建课堂</el-button>
<el-button type="success" @click="createClasscourse(true)">公屏上课</el-button>
</div>
</template>
<!-- 故障备用 -->
@ -146,7 +147,7 @@ const open = async (id, classObj) => {
await getAptInfo(id)
//
getClassList()
console.log('classObj', classObj)
// console.log('classObj', classObj)
//
if (!!classObj) {
dt.ctCourse = classObj
@ -245,8 +246,8 @@ const getClasscourseList = async type => {
}
}
}
//
const createClasscourse = async () => {
// isPublic
const createClasscourse = async (isPublic = false) => {
const { classid } = classForm.form
if (!classid) {
ElMessage.warning('请选择班级')
@ -255,8 +256,8 @@ const createClasscourse = async () => {
dt.loading = true
const { entpcourseid, evalid, id, coursetitle } = myClassActive.value //
const curDate = commUtil.getDateNow('yyyy-MM-dd')
const params = {
id: 0, coursetype: '', courseverid: 0, coursedesc: '', status: '',
const params = { // status = open
id: 0, coursetype: '', courseverid: 0, coursedesc: '', status: isPublic?'open':'',
teacherid: userStore.id, entpcoursefileid: id, classid,
entpcourseid, evalid, coursetitle,
plandate: curDate, opendate: curDate
@ -274,7 +275,7 @@ const createClasscourse = async () => {
setTimeout(async() => {
msgEl.close()
const res = await Http_Classcourse.getClasscourse(teacherForm.form.classcourseid)
openPublicScreen(res.data)
openPublicScreen(res.data, isPublic)
}, 2000);
}, 1000);
}
@ -355,7 +356,7 @@ const getQrUrl = async() => {
}
//
const openPublicScreen = (classcourse) => {
const openPublicScreen = (classcourse, isPublic) => {
console.log('打开公屏', classcourse)
if (!dt.ctCourse) { // -
// app
@ -366,11 +367,14 @@ const openPublicScreen = (classcourse) => {
const resource = toRaw(myClassActive.value)
sessionStore.set('curr.resource', resource) //
sessionStore.set('curr.classcourse', classcourse) //
//
sessionStore.set('curr.isPublic', isPublic) //
createWindow('open-win', {
url: '/pptist', //
close: () => {
sessionStore.set('curr.resource', null) //
sessionStore.set('curr.classcourse', null) //
sessionStore.set('curr.isPublic', null) //
}
})
visible.value = false //

View File

@ -10,6 +10,7 @@
<el-dropdown-menu>
<el-dropdown-item @click="createAIPPT">新建文枢课件</el-dropdown-item>
<el-dropdown-item @click="aiTOPPT">AI一键生成</el-dropdown-item>
<el-dropdown-item @click="openGridPic">打开宫格</el-dropdown-item>
<el-dropdown-item @click="openFilePicker">导入PPT</el-dropdown-item>
<input type="file" ref="fileInput" style="display: none;" @change="handleFileChange" accept="application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation">
</el-dropdown-menu>
@ -341,6 +342,14 @@ export default {
// }
// },
methods: {
openGridPic() {
createWindow('open-win', {
url: '/gridPic', //
option: {
maximizable: true
}
})
},
//
sleep(ms){return new Promise(resolve => setTimeout(resolve, ms))},
addAiPPT(item) {
@ -355,8 +364,8 @@ export default {
//
startClass(item, classObj) {
// ()
// const id = sessionStore.has('activeClass.id') ? sessionStore.get('activeClass.id') : null
// if (id && id == item.id) return ElMessage.warning('')
const iscourse = !!sessionStore.get('curr.classcourse')
if (iscourse) return ElMessage.warning('公屏已打开,请勿重复操作')
// -store
sessionStore.set('activeClass', item)
this.activeClass = item
@ -367,7 +376,8 @@ export default {
this.$refs.calssRef.open(item.fileId, classObj)
}
if(item.fileFlag === 'aippt') {
this.$refs.calssRef.open(item.fileId, classObj)
if (!!classObj) this.changeClass('continue', classObj) //
else this.$refs.calssRef.open(item.fileId, classObj) //
}
},
// -apt
@ -375,7 +385,19 @@ export default {
switch(type) {
case 'continue': { //
const aptFileId = row.entpcoursefileid
this.$refs.calssRef.open(aptFileId, row)
const res = await getEntpcoursefile(aptFileId)
if (res.code == 200) {
const resource = res.data
if (resource.filetype != 'aippt') this.$refs.calssRef.open(aptFileId, row)
else {
if (!!sessionStore.get('curr.classcourse')) return ElMessage.warning('公屏已打开,请勿重复操作')
const msgEl = ElMessage.warning({message:'正在打开公屏,请稍后...',duration: 0})
setTimeout(()=>{
msgEl.close()
this.openPublicScreen('class', resource, row) // -
}, 2000)
}
} else ElMessage.error(res.msg||'获取课件信息失败')
break
}
case 'close': { //
@ -428,16 +450,7 @@ export default {
if (row.fileFlag === 'aippt' && !!row.fileId) {
const res = await getEntpcoursefile(row.fileId)
if (res && res.code === 200) {
sessionStore.set('curr.resource', res.data) //
sessionStore.set('curr.smarttalk', row) // smarttalk
createWindow('open-win', {
url: '/pptist', //
close: () => {
sessionStore.set('curr.resource', null) //
sessionStore.set('curr.smarttalk', null) //
this.asyncAllFile() //
}
})
this.openPublicScreen('edit', res.data, row) // -
} else {
ElMessage.warning(res.msg||'文件获取异常!')
}
@ -448,6 +461,8 @@ export default {
}
case 'wsApp': { // app
// console.log('wsApp', row)
window.test = sessionStore
if (!!sessionStore.get('curr.classcourse')) return ElMessage.warning('公屏已打开,请勿重复操作')
const head = MsgEnum.HEADS.MSG_0000
const data = { id: row.id }
const type = ChatWs.TYPES.single
@ -462,24 +477,38 @@ export default {
msgEl.close() //
const resource = res?.data||{}
const classcourse = row
sessionStore.set('curr.resource', resource) //
sessionStore.set('curr.classcourse', classcourse) //
createWindow('open-win', {
url: '/pptist', //
close: () => {
sessionStore.set('curr.resource', null) //
sessionStore.set('curr.classcourse', null) //
}
})
this.openPublicScreen('class',resource, classcourse) // -
break
}
default:
break
}
},
/**
* description 打开公屏
* @param {string} type 类型 edit 打开 class 上课
* @param {object} resource 资源信息
* @param {object} currData 当前数据 type: edit/class 备课信息 | 课堂信息
*/
openPublicScreen(type, resource, currData) {
sessionStore.set('curr.resource', resource) //
if (type=='edit') sessionStore.set('curr.smarttalk', currData) // smarttalk
else sessionStore.set('curr.classcourse', currData) //
createWindow('open-win', {
url: '/pptist', //
close: () => {
sessionStore.set('curr.resource', null) //
if (type=='edit') {
sessionStore.set('curr.smarttalk', null) //
this.asyncAllFile() //
} else sessionStore.set('curr.classcourse', null) //
}
})
},
closeChange() { // -
// console.log('')
// this.activeClass = null
this.activeClass = null
sessionStore.delete('activeClass')
},
initReserv(id) {
@ -895,6 +924,7 @@ export default {
return getSmarttalkPage({
...this.uploadData,
orderByColumn: 'createTime',
fileFlag: 'aippt',
isAsc: 'desc',
pageSize: 500
})

View File

@ -248,6 +248,8 @@ defineExpose({ trigger })
position: fixed;
// height: 90vh;
// border: 1px solid;
z-index: 99;
pointer-events: none;
inset: auto auto 3em 1em;
display: flex;
gap: 10px;