123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260 |
- <template>
- <div class="pptist-student-viewer" :class="{ 'fullscreen': isFullscreen, 'laser-pen': laserPen }">
- <!-- 左侧导航栏 -->
- <div class="layout-content-left" v-show="type == '1'">
- <div class="thumbnails">
- <div class="viewer-header">
- <h3>幻灯片导航</h3>
- </div>
- <div class="thumbnail-list">
- <div v-for="(slide, index) in slides" :key="slide.id" class="thumbnail-item"
- :class="{ 'active': slideIndex === index }" @click="goToSlide(index)">
- <div class="label">{{ fillDigit(index + 1, 2) }}</div>
- <ThumbnailSlide class="thumbnail" :slide="slide" :size="168" :visible="true" @click="goToSlide(index)" />
- </div>
- </div>
- <!-- <div class="page-number">幻灯片 {{ slideIndex + 1 }} / {{ slides.length }}</div>
- <div class="progress-bar">
- <div class="progress-fill" :style="{ width: `${((slideIndex + 1) / slides.length) * 100}%` }"></div>
- </div> -->
- </div>
- </div>
- <!-- 中间放映区域 -->
- <div class="layout-content-center">
- <div class="viewer-header" :class="{ 'hidden': isFullscreen }">
- <div class="slide-title">幻灯片 {{ slideIndex + 1 }}</div>
- <div class="viewer-controls">
- <button @click="previousSlide" :disabled="slideIndex === 0" title="上一页">
- <IconLeftTwo class="control-icon" />
- </button>
- <button @click="nextSlide" :disabled="slideIndex === slides.length - 1" title="下一页">
- <IconRightTwo class="control-icon" />
- </button>
- <!-- <button @click="resetZoom" title="重置缩放">
- <IconUndo class="control-icon" />
- </button> -->
- <button @click="enterFullscreen" title="全屏">
- <IconFullScreenOne class="control-icon" />
- </button>
- <!-- <button @click="backToEditor" class="back-btn" title="返回编辑">
- <IconEdit class="control-icon" />
- </button> -->
- </div>
- </div>
- <div class="viewer-canvas" ref="viewerCanvasRef">
- <!-- 全屏时:使用放映功能 -->
- <!-- <ScreenSlideList :slideWidth="slideWidth"
- :slideHeight="slideHeight" :animationIndex="0" :turnSlideToId="() => { }"
- :manualExitFullscreen="() => { }" /> -->
- <!-- 不全屏时:使用编辑模式的显示比例和居中逻辑 -->
- <div class="slide-list-wrap" :style="{
- width: isFullscreen ? '100%' : (slideWidth * canvasScale) + 'px',
- height: isFullscreen ? '100%' : (slideHeight * canvasScale) + 'px',
- left: isFullscreen ? '0' : `${(containerWidth - slideWidth * canvasScale) / 2}px`,
- top: isFullscreen ? '0' : `${(containerHeight - slideHeight * canvasScale) / 2}px`
- }">
- <div class="viewport" v-if="false">
- <div class="background" :style="backgroundStyle"></div>
- <ScreenElement v-for="(element, index) in elementList" :key="element.id" :elementInfo="element"
- :elementIndex="index + 1" :animationIndex="0" :turnSlideToId="() => { }"
- :manualExitFullscreen="() => { }" />
- </div>
- <ScreenSlideList :slideWidth="slideWidth * canvasScale" :slideHeight="slideHeight * canvasScale"
- :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }" />
- </div>
- <!-- 全屏时的左右下角工具按钮 -->
- <div v-if="isFullscreen" class="tools-left">
- <IconLeftTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="previousSlide" />
- <IconRightTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="nextSlide" />
- </div>
- <!-- 作业提交按钮 - 当当前幻灯片包含iframe时显示 -->
- <div v-if="currentSlideHasIframe" class="homework-submit-btn" :class="{ 'submitting': isSubmitting }"
- :style="{ right: getHomeworkButtonRight() + 'px' }" @click="handleHomeworkSubmit"
- v-tooltip="isSubmitting ? '作业提交中...' : '作业提交'">
- <IconEdit v-if="!isSubmitting" class="tool-btn" />
- <div v-else class="loading-spinner"></div>
- <span class="btn-text">{{ isSubmitting ? '提交中...' : '作业提交' }}</span>
- </div>
- <!-- 功能组件 -->
- <SlideThumbnails v-if="slideThumbnailModelVisible" :turnSlideToIndex="goToSlide"
- @close="slideThumbnailModelVisible = false" />
- <WritingBoardTool :slideWidth="slideWidth" :slideHeight="slideHeight" v-if="writingBoardToolVisible"
- @close="writingBoardToolVisible = false" />
- <CountdownTimer v-if="timerlVisible" @close="timerlVisible = false" />
- <div v-if="isFullscreen" class="tools-right" :class="{ 'visible': rightToolsVisible }"
- @mouseleave="rightToolsVisible = false" @mouseenter="rightToolsVisible = true">
- <div class="content">
- <div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{ slideIndex +
- 1 }} / {{ slides.length }}</div>
- <IconWrite class="tool-btn" v-tooltip="'画笔工具'" @click="writingBoardToolVisible = true" />
- <IconMagic class="tool-btn" v-tooltip="'激光笔'" :class="{ 'active': laserPen }" @click="toggleLaserPen" />
- <IconStopwatchStart class="tool-btn" v-tooltip="'计时器'" @click="timerlVisible = !timerlVisible" />
- <IconOffScreenOne class="tool-btn" v-tooltip="'退出全屏'" @click="enterFullscreen" />
- </div>
- </div>
- </div>
- </div>
- <div class="layout-content-right" v-show="type == '1'">
- <div class="thumbnails">
- <div class="viewer-header">
- <h3>作业区</h3>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script lang="ts" setup>
- import { computed, ref, onMounted, onUnmounted, nextTick, inject, watch } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useSlidesStore } from '@/store'
- import { ElementTypes } from '@/types/slides'
- import { fillDigit } from '@/utils/common'
- import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
- import ScreenSlideList from '@/views/Screen/ScreenSlideList.vue'
- import ScreenElement from '@/views/Screen/ScreenElement.vue'
- import SlideThumbnails from '@/views/Screen/SlideThumbnails.vue'
- import WritingBoardTool from '@/views/Screen/WritingBoardTool.vue'
- import CountdownTimer from '@/views/Screen/CountdownTimer.vue'
- import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
- import useImport from '@/hooks/useImport'
- import message from '@/utils/message'
- import api from '@/services/course'
- // 定义组件props
- interface Props {
- courseid?: string | null
- userid?: string | null
- oid?: string | null
- org?: string | null
- cid?: string | null
- type?: string | null
- }
- const props = withDefaults(defineProps<Props>(), {
- courseid: null,
- userid: null,
- oid: null,
- org: null,
- cid: null,
- type: null,
- })
- // 图标组件通过全局注册,无需导入
- const slidesStore = useSlidesStore()
- const { slides, slideIndex, currentSlide, viewportSize, viewportRatio } = storeToRefs(slidesStore)
- // 添加容器引用,用于计算幻灯片尺寸
- const viewerCanvasRef = ref<HTMLElement>()
- // 放映相关的状态
- const canvasScale = ref(1) // 画布缩放比例
- const isFullscreen = ref(false) // 是否全屏
- const containerWidth = ref(0) // 容器宽度
- const containerHeight = ref(0) // 容器高度
- // 全屏工具相关状态
- const rightToolsVisible = ref(false)
- const writingBoardToolVisible = ref(false)
- const timerlVisible = ref(false)
- const slideThumbnailModelVisible = ref(false)
- const laserPen = ref(false)
- // 作业提交状态
- const isSubmitting = ref(false)
- // 控制组件显示的开关
- const showSlideList = ref(true)
- const slideWidth = ref(0)
- const slideHeight = ref(0)
- // 计算幻灯片尺寸的函数
- const calculateSlideSize = () => {
- const slideWrapRef = isFullscreen.value ? document.body : viewerCanvasRef.value
- const winWidth = slideWrapRef.clientWidth
- const winHeight = slideWrapRef.clientHeight
- // 根据视口比例计算最佳尺寸
- if (winHeight / winWidth === viewportRatio.value) {
- slideWidth.value = winWidth
- slideHeight.value = winHeight
- }
- else if (winHeight / winWidth > viewportRatio.value) {
- slideWidth.value = winWidth
- slideHeight.value = winWidth * viewportRatio.value
- }
- else {
- slideWidth.value = winHeight / viewportRatio.value
- slideHeight.value = winHeight
- }
- }
- // 使用编辑模式的缩放逻辑
- const calculateScale = () => {
- console.log('calculateScale 开始执行')
- // 计算幻灯片尺寸
- calculateSlideSize()
- // 获取容器尺寸
- const container = viewerCanvasRef.value || document.querySelector('.viewer-canvas')
- if (container) {
- containerWidth.value = container.clientWidth
- containerHeight.value = container.clientHeight
- console.log('容器尺寸:', {
- width: containerWidth.value,
- height: containerHeight.value
- })
- // 计算基础尺寸
- const baseWidth = viewportSize.value
- const baseHeight = viewportSize.value * viewportRatio.value
- console.log('基础尺寸:', {
- baseWidth,
- baseHeight,
- viewportSize: viewportSize.value,
- viewportRatio: viewportRatio.value
- })
- // 计算缩放比例,让幻灯片能够合理利用空间
- const scaleX = containerWidth.value / baseWidth
- const scaleY = containerHeight.value / baseHeight
- console.log('原始缩放比例:', { scaleX, scaleY })
- // 选择较小的缩放比例,确保幻灯片完全显示且居中,留10%边距
- const scale = Math.min(scaleX, scaleY) * 0.9
- console.log('最终缩放比例:', scale)
- canvasScale.value = isFullscreen.value ? 1 : props.type == '1' ? 0.9 : 0.95
- }
- else {
- console.error('找不到容器元素')
- }
- }
- // 简化:直接使用放映功能的缩放逻辑
- const resetZoom = () => {
- calculateScale()
- }
- // 背景样式
- const background = computed(() => currentSlide.value?.background)
- const { backgroundStyle } = useSlideBackgroundStyle(background)
- // 计算当前幻灯片的元素列表
- const elementList = computed(() => {
- return currentSlide.value?.elements || []
- })
- // 检测当前幻灯片是否包含iframe元素
- const currentSlideHasIframe = computed(() => {
- return elementList.value.some(element => element.type === ElementTypes.FRAME)
- })
- // 跳转到指定幻灯片
- const goToSlide = (index: number) => {
- console.log('goToSlide 被调用,目标索引:', index)
- console.log('当前索引:', slideIndex.value)
- slidesStore.updateSlideIndex(index)
- console.log('更新后的索引:', slideIndex.value)
- }
- // 上一页
- const previousSlide = () => {
- if (slideIndex.value > 0) {
- slidesStore.updateSlideIndex(slideIndex.value - 1)
- }
- }
- // 下一页
- const nextSlide = () => {
- if (slideIndex.value < slides.value.length - 1) {
- slidesStore.updateSlideIndex(slideIndex.value + 1)
- }
- }
- // 全屏
- const enterFullscreen = () => {
- if (document.fullscreenElement) {
- document.exitFullscreen()
- }
- else {
- document.documentElement.requestFullscreen()
- }
- }
- // 监听全屏状态变化
- const handleFullscreenChange = () => {
- isFullscreen.value = !!document.fullscreenElement
- if (isFullscreen.value) {
- // 全屏时不需要计算缩放,直接使用放映功能
- console.log('进入全屏模式')
- }
- else {
- // 退出全屏时重置所有工具状态并重新计算缩放比例
- console.log('退出全屏模式,重置工具状态')
- // 重置所有工具状态
- rightToolsVisible.value = false
- writingBoardToolVisible.value = false
- timerlVisible.value = false
- slideThumbnailModelVisible.value = false
- laserPen.value = false
- // 重新计算缩放比例
- nextTick(() => {
- calculateScale()
- })
- }
- }
- // 切换激光笔模式
- const toggleLaserPen = () => {
- laserPen.value = !laserPen.value
- console.log('激光笔状态:', laserPen.value ? '开启' : '关闭')
- }
- // 获取导入导出功能
- const { readJSON, exportJSON2 } = useImport()
- // 根据iframe的URL查找对应的幻灯片索引
- const findSlideIndexByIframeUrl = (iframeUrl: string): number => {
- try {
- console.log('查找iframe对应的幻灯片索引,iframe URL:', iframeUrl)
- // 遍历所有幻灯片,查找包含该iframe URL的幻灯片
- for (let i = 0; i < slides.value.length; i++) {
- const slide = slides.value[i]
- // 检查幻灯片的元素中是否有iframe
- if (slide.elements && slide.elements.length > 0) {
- for (const element of slide.elements) {
- // 检查是否是iframe元素
- if (element.type === ElementTypes.FRAME) {
- // 检查iframe的src是否匹配
- if (element.url === iframeUrl) {
- console.log(`找到匹配的幻灯片,索引: ${i}, 幻灯片ID: ${slide.id}`)
- return i
- }
- }
- }
- }
- }
- // 如果没有找到匹配的幻灯片,返回当前幻灯片索引
- console.log('未找到匹配的幻灯片,使用当前幻灯片索引:', slideIndex.value)
- return slideIndex.value
- }
- catch (error) {
- console.error('查找幻灯片索引时出错:', error)
- return slideIndex.value
- }
- }
- // 处理iframe链接,为包含workPage的iframe添加必要参数
- const processIframeLinks = () => {
- try {
- console.log('开始处理iframe链接')
- console.log('当前props:', { courseid: props.courseid, userid: props.userid })
- // 从slides数据中查找包含iframe的元素
- let hasIframe = false
- const updatedSlides = slides.value.map((slide, slideIndex) => {
- if (slide.elements && slide.elements.length > 0) {
- const updatedElements = slide.elements.map(element => {
- // 检查是否是iframe元素
- if (element.type === ElementTypes.FRAME && element.url) {
- const iframeSrc = element.url
-
- if (iframeSrc.includes('workPage')) {
- hasIframe = true
- console.log(`处理幻灯片 ${slideIndex + 1} 中的iframe链接:`, iframeSrc)
- try {
- // 解析URL,处理hash部分
- let baseUrl = iframeSrc
- let hashPart = ''
- // 分离base URL和hash部分
- if (iframeSrc.includes('#')) {
- const parts = iframeSrc.split('#')
- baseUrl = parts[0]
- hashPart = parts[1]
- }
- // 构建新的hash部分,添加参数
- // 使用当前幻灯片索引作为task参数
- let newHash = hashPart
- if (newHash.includes('?')) {
- // 如果hash中已经有查询参数,添加&
- newHash += `&courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
- }
- else {
- // 如果hash中没有查询参数,添加?
- newHash += `?courseid=${props.courseid || ''}&userid=${props.userid || ''}&stage=0&task=${slideIndex}&tool=0`
- }
- // 构建新的URL
- const newUrl = `${baseUrl}#${newHash}`
- console.log(`幻灯片 ${slideIndex + 1} 的iframe链接已更新:`, newUrl)
- // 返回更新后的元素
- return {
- ...element,
- url: newUrl
- }
- }
- catch (error) {
- console.error(`处理幻灯片 ${slideIndex + 1} 的iframe链接时出错:`, error)
- return element
- }
- }
- }
-
- // 不是iframe元素或不需要处理,直接返回
- return element
- })
- // 返回更新后的幻灯片
- return {
- ...slide,
- elements: updatedElements
- }
- }
-
- // 没有元素的幻灯片直接返回
- return slide
- })
- if (hasIframe) {
- console.log('找到iframe元素,更新slides数据')
- // 更新store中的slides数据
- slidesStore.setSlides(updatedSlides)
- console.log('slides数据更新完成')
- }
- else {
- console.log('未找到包含workPage的iframe元素')
- }
- console.log('iframe链接处理完成')
- }
- catch (error) {
- console.error('处理iframe链接时出错:', error)
- }
- }
- // 导入JSON功能
- const importJSON = (jsonData: any) => {
- try {
- console.log('Student importJSON 开始执行')
- const result = readJSON(jsonData, true)
- if (result.success) {
- console.log('Student importJSON 成功,开始重新渲染')
- // 强制重新渲染:先隐藏组件
- showSlideList.value = false
- // 重新计算画布尺寸和缩放比例
- nextTick(() => {
- calculateScale()
- // 延迟500ms后重新显示组件,确保重新渲染完成
- setTimeout(() => {
- showSlideList.value = true
- console.log('组件重新渲染完成')
- }, 500)
- })
- return true
- }
- console.error('Student importJSON 失败:', result.error)
- return false
- }
- catch (error) {
- console.error('Student importJSON 执行失败:', error)
- return false
- }
- }
- // 导出JSON功能
- const exportJSON = () => {
- try {
- console.log('Student exportJSON 开始执行,调用 useImport.exportJSON2')
- // 直接调用 useImport 中的 exportJSON2 函数
- const exportData = exportJSON2()
- if (exportData) {
- return exportData
- }
- console.error('Student exportJSON 失败: exportJSON2 返回空数据')
- return false
- }
- catch (error) {
- console.error('Student exportJSON 执行失败:', error)
- return false
- }
- }
- // 返回编辑器
- const backToEditor = () => {
- // 通过路由跳转到编辑模式
- window.location.href = '/'
- }
- const submitWork = async (slideIndex: number, atool: string, content: string, type: string) => {
- const res = await api.submitWork({
- uid: props.userid as string,
- cid: props.courseid as string,
- stage: '0',
- task: String(slideIndex), // 转为字符串
- tool: '0',
- atool: atool,
- content: content,
- type: type
- })
- console.log(res)
- }
- // 作业提交功能(优化版)
- const handleHomeworkSubmit = async () => {
- console.log('作业提交按钮被点击')
- // 防抖:如果正在提交中,直接返回
- if (isSubmitting.value) {
- console.log('作业正在提交中,忽略重复点击')
- return
- }
- isSubmitting.value = true
- try {
- // 获取所有iframe元素
- const iframes = document.querySelectorAll('.viewer-canvas iframe')
- console.log('找到iframe元素数量:', iframes.length)
- if (iframes.length === 0) {
- message.warning('当前页面没有找到iframe元素')
- return
- }
- let hasSubmitWork = false
- for (let i = 0; i < iframes.length; i++) {
- const iframe = iframes[i] as HTMLIFrameElement
- const iframeSrc = iframe.src
- console.log(`iframe ${i + 1} 链接:`, iframeSrc)
- // 检查iframe链接是否包含workPage
- if (iframeSrc && iframeSrc.includes('workPage')) {
- console.log('找到包含workPage的iframe,尝试执行submitWork')
- try {
- const iframeWindow = iframe.contentWindow as Window & { submitWork?: (...args: any[]) => unknown }
- if (iframeWindow && typeof iframeWindow.submitWork === 'function') {
- console.log('执行iframe中的submitWork方法,参数可变')
- const iframeSlideIndex = slideIndex.value
- const submitArgs = [iframeSlideIndex]
- // 支持同步和异步submitWork
- const result = iframeWindow.submitWork(...submitArgs)
- if (result instanceof Promise) {
- await result
- console.log('submitWork异步执行完成')
- }
- else {
- console.log('submitWork同步执行完成')
- }
- message.success('作业提交成功')
- hasSubmitWork = true
- break
- }
- else {
- console.log('iframe中没有找到submitWork方法')
- }
- }
- catch (error) {
- console.error('访问iframe内容时出错:', error)
- }
- }
- else if (iframeSrc && (iframeSrc.includes('aichat.cocorobo') || iframeSrc.includes('knowledge.cocorobo'))) {
- console.log('找到包含aichat.cocorobo或knowledge.cocorobo的iframe,尝试执行submitWork')
- // 由于TS类型检查,需通过 any 绕过类型限制
- const iframeWindow = iframe.contentWindow as any
- if (iframeWindow && iframeWindow.exposed_outputs) {
- console.log('执行iframe中的submitWork方法,参数可变')
- const iframeSlideIndex = slideIndex.value
- const Cow = JSON.stringify(iframeWindow.exposed_outputs)
- // 这里假设 submitWork 是全局可用的函数
- await submitWork(iframeSlideIndex, '72', Cow, '20')
- message.success('作业提交成功')
- hasSubmitWork = true
- }
- }
- else {
- console.log('尝试截图当前页面并提交')
- try {
- // 导入html-to-image库
- const { toPng } = await import('html-to-image')
-
- // 截图当前页面
- const imageData = await toPng(document.querySelector('.viewer-canvas') as HTMLElement, {
- quality: 0.95,
- backgroundColor: '#ffffff'
- })
-
- // 提交截图
- await submitWork(slideIndex.value, '73', imageData, '1') // 73表示截图工具,21表示图片类型
- message.success('页面截图提交成功')
- hasSubmitWork = true
- }
- catch (error) {
- console.error('截图提交失败:', error)
- message.error('截图提交失败')
- }
- }
- }
- if (!hasSubmitWork) {
- message.info('未找到可用的作业提交功能')
- }
- }
- catch (error) {
- console.error('作业提交过程中出错:', error)
- message.error('作业提交失败')
- }
- finally {
- isSubmitting.value = false
- }
- }
- // 获取作业提交按钮的右侧位置
- const getHomeworkButtonRight = () => {
- if (isFullscreen.value) {
- return 30 // 全屏时按钮在右侧30px
- }
- if (props.type === '1') {
- return 230 // type=1时(有左侧导航栏)按钮在右侧230px
- }
- return 30 // type=2时按钮在右侧30px
- }
- // 键盘快捷键
- const handleKeydown = (e: KeyboardEvent) => {
- switch (e.key) {
- case 'ArrowLeft':
- case 'PageUp':
- e.preventDefault()
- previousSlide()
- break
- case 'ArrowRight':
- case 'PageDown':
- case ' ':
- e.preventDefault()
- nextSlide()
- break
- case 'F11':
- e.preventDefault()
- enterFullscreen()
- break
- case 'Escape':
- if (document.fullscreenElement) {
- document.exitFullscreen()
- }
- break
- default:
- break
- }
- }
- // 事件处理函数
- const handleSlidesDataUpdated = () => {
- console.log('收到 slidesDataUpdated 事件')
- // 强制重新渲染:先隐藏组件
- showSlideList.value = false
- nextTick(() => {
- calculateScale()
- // 延迟500ms后重新显示组件,确保重新渲染完成
- setTimeout(() => {
- showSlideList.value = true
- console.log('组件重新渲染完成')
- // 重新处理iframe链接
- processIframeLinks()
- }, 500)
- console.log('slidesDataUpdated 事件处理完成')
- })
- }
- const handleViewportSizeUpdated = (event: any) => {
- console.log('收到 viewportSizeUpdated 事件:', event.detail)
- // 重新计算缩放比例
- nextTick(() => {
- calculateScale()
- console.log('viewportSizeUpdated 事件处理完成')
- })
- }
- const getCourseDetail = async () => {
- const res = await api.getCourseDetail(props.courseid as string)
- console.log(res)
- const courseDetail = res[0][0]
- const pptdata = JSON.parse(courseDetail.chapters).pptData ? JSON.parse(courseDetail.chapters).pptData : []
- importJSON(pptdata)
- }
- onMounted(() => {
- document.addEventListener('keydown', handleKeydown)
- // 处理URL参数
- if (props.courseid || props.type) {
- console.log('收到URL参数:', { courseid: props.courseid, type: props.type })
- // 这里可以根据courseid和type进行相应的处理
- // 比如加载特定的课程数据、设置特定的显示模式等
- if (props.courseid) {
- console.log('课程ID:', props.courseid)
- // TODO: 根据courseid加载对应的课程数据
- }
- if (props.type) {
- console.log('类型:', props.type)
- // TODO: 根据type设置特定的显示模式或功能
- }
- }
- getCourseDetail()
- // 计算初始缩放比例
- nextTick(() => {
- calculateScale()
- // 处理iframe链接
- processIframeLinks()
- })
- // 监听窗口大小变化
- window.addEventListener('resize', calculateScale)
- // 监听全屏状态变化
- document.addEventListener('fullscreenchange', handleFullscreenChange)
- // 监听幻灯片数据更新事件(来自useImport的readJSON)
- window.addEventListener('slidesDataUpdated', handleSlidesDataUpdated)
- // 监听视口尺寸更新事件
- window.addEventListener('viewportSizeUpdated', handleViewportSizeUpdated)
- // 将导入导出功能暴露到window上,方便调试和外部调用
- ; (window as any).PPTistStudent = {
- importJSON,
- exportJSON,
- slides: slidesStore.slides,
- currentSlide: computed(() => slidesStore.currentSlide),
- slideIndex: computed(() => slidesStore.slideIndex),
- goToSlide,
- previousSlide,
- nextSlide,
- enterFullscreen,
- toggleLaserPen,
- // 添加URL参数到全局对象中
- courseid: props.courseid,
- type: props.type
- }
- console.log('PPTist Student View 已加载,可通过 window.PPTistStudent 访问功能')
- console.log('URL参数:', { courseid: props.courseid, type: props.type })
- })
- onUnmounted(() => {
- document.removeEventListener('keydown', handleKeydown)
- window.removeEventListener('resize', calculateScale)
- document.removeEventListener('fullscreenchange', handleFullscreenChange)
- // 移除幻灯片数据更新事件监听器
- window.removeEventListener('slidesDataUpdated', handleSlidesDataUpdated)
- // 移除视口尺寸更新事件监听器
- window.removeEventListener('viewportSizeUpdated', handleViewportSizeUpdated)
- // 清理window上的引用
- if ((window as any).PPTistStudent) {
- delete (window as any).PPTistStudent
- console.log('PPTist Student View 已卸载,window.PPTistStudent 已清理')
- }
- })
- </script>
- <style lang="scss" scoped>
- .pptist-student-viewer {
- height: 100vh;
- display: flex;
- background-color: #f5f5f5;
- // 全屏模式样式
- &.fullscreen {
- .layout-content-left {
- display: none; // 全屏时隐藏左侧导航栏
- }
- .layout-content-right {
- display: none; // 全屏时隐藏左侧导航栏
- }
- .viewer-header {
- display: none; // 全屏时隐藏顶部标题栏
- }
- }
- // 激光笔模式样式
- &.laser-pen {
- cursor: url(data:image/png;base64,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) 20 20, default !important;
- }
- }
- .layout-content-left {
- width: 200px;
- height: 100%;
- background-color: #fff;
- border-right: 1px solid #e0e0e0;
- overflow-y: auto;
- }
- .layout-content-right {
- width: 200px;
- height: 100%;
- background-color: #fff;
- border-left: 1px solid #e0e0e0;
- overflow-y: auto;
- }
- .thumbnails {
- padding: 0;
- .viewer-header {
- margin-bottom: 16px;
- h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- color: #333;
- }
- }
- .thumbnail-list {
- width: 100%;
- padding: 0 16px;
- box-sizing: border-box;
- .thumbnail-item {
- position: relative;
- margin-bottom: 12px;
- cursor: pointer;
- border-radius: 8px;
- overflow: hidden;
- transition: all 0.2s ease;
- border: 2px solid rgba(24, 144, 255, 0.2);
- &:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- border-color: rgba(24, 144, 255, 0.4);
- }
- &.active {
- border: 2px solid #1890ff;
- box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
- }
- .label {
- position: absolute;
- top: 8px;
- left: 8px;
- background-color: rgba(0, 0, 0, 0.6);
- color: #fff;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 12px;
- font-weight: 600;
- z-index: 1;
- }
- .thumbnail {
- width: 100%;
- height: auto;
- }
- }
- }
- .page-number {
- text-align: center;
- margin-top: 16px;
- padding: 8px;
- background-color: #f0f0f0;
- border-radius: 4px;
- font-size: 14px;
- color: #666;
- }
- .progress-bar {
- margin-top: 12px;
- height: 6px;
- background-color: #f0f0f0;
- border-radius: 3px;
- overflow: hidden;
- .progress-fill {
- height: 100%;
- background: linear-gradient(90deg, #1890ff, #40a9ff);
- border-radius: 3px;
- transition: width 0.3s ease;
- }
- }
- }
- .layout-content-center {
- flex: 1;
- display: flex;
- flex-direction: column;
- background-color: #000;
- }
- .viewer-header {
- height: 60px;
- background-color: #fff;
- border-bottom: 1px solid #e0e0e0;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 24px;
- transition: transform 0.3s ease;
- &.hidden {
- transform: translateY(-100%);
- }
- .slide-title {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- }
- .viewer-controls {
- display: flex;
- gap: 12px;
- button {
- padding: 8px 12px;
- border: 1px solid #d9d9d9;
- border-radius: 6px;
- background-color: #fff;
- color: #333;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 40px;
- height: 36px;
- &:hover:not(:disabled) {
- border-color: #1890ff;
- color: #1890ff;
- }
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- &.back-btn {
- background-color: #1890ff;
- color: #fff;
- border-color: #1890ff;
- &:hover {
- background-color: #40a9ff;
- border-color: #40a9ff;
- }
- }
- .control-icon {
- font-size: 16px;
- }
- }
- }
- }
- .viewer-canvas {
- flex: 1;
- position: relative;
- background-color: rgb(249, 249, 249);
- overflow: hidden;
- // 全屏时隐藏滚动条和边框
- &.fullscreen-mode {
- overflow: hidden !important;
- background-color: transparent !important;
- }
- }
- .slide-list-wrap {
- position: absolute;
- overflow: hidden;
- background-color: #fff;
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 0 0 12px 0 rgba(0, 0, 0, 0.1);
- border-radius: 8px;
- /* 全屏时去掉圆角 */
- .pptist-student-viewer.fullscreen & {
- border-radius: 0;
- }
- }
- .loading-indicator {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- .loading-text {
- color: #666;
- font-size: 14px;
- background-color: rgba(255, 255, 255, 0.9);
- padding: 12px 20px;
- border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- }
- // 全屏模式样式
- .fullscreen-slide {
- // 使用放映功能的默认样式
- }
- // 全屏工具按钮样式,直接复制放映功能的样式
- .tools-left {
- position: fixed;
- bottom: 8px;
- left: 8px;
- font-size: 25px;
- color: #666;
- z-index: 10;
- .tool-btn {
- opacity: .3;
- cursor: pointer;
- transition: opacity 0.3s;
- &:hover {
- opacity: .95;
- }
- &+.tool-btn {
- margin-left: 8px;
- }
- }
- }
- .tools-right {
- height: 66px;
- position: fixed;
- bottom: -66px;
- right: 0;
- z-index: 5;
- padding: 8px;
- transition: bottom 0.3s;
- &.visible {
- bottom: 0;
- }
- &::after {
- content: '';
- width: 100%;
- height: 66px;
- position: absolute;
- left: 0;
- top: -66px;
- }
- .content {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 4px;
- font-size: 25px;
- background-color: #fff;
- color: #333;
- padding: 8px 10px;
- box-shadow: 0 2px 12px 0 rgb(56, 56, 56, .2);
- border: 1px solid #e2e6ed;
- }
- .tool-btn {
- cursor: pointer;
- &:hover,
- &.active {
- color: #1890ff;
- }
- &+.tool-btn {
- margin-left: 15px;
- }
- }
- .page-number {
- font-size: 12px;
- padding: 0 12px;
- cursor: pointer;
- }
- }
- .viewport {
- position: relative;
- width: 100%;
- height: 100%;
- background-color: #fff;
- }
- .background {
- width: 100%;
- height: 100%;
- background-position: center;
- position: absolute;
- }
- // 响应式设计
- @media (max-width: 768px) {
- .layout-content-left {
- width: 160px;
- }
- .layout-content-right {
- width: 160px;
- }
- .viewer-header {
- padding: 0 16px;
- .slide-title {
- font-size: 16px;
- }
- .viewer-controls button {
- padding: 6px 12px;
- font-size: 14px;
- }
- }
- }
- /* 作业提交按钮样式 */
- .homework-submit-btn {
- position: fixed;
- bottom: 30px;
- z-index: 100;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 12px 20px;
- border-radius: 25px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 8px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
- transition: all 0.3s ease;
- font-size: 14px;
- font-weight: 500;
- &:hover:not(.submitting) {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
- }
- &:active:not(.submitting) {
- transform: translateY(0);
- }
- &.submitting {
- cursor: not-allowed;
- opacity: 0.8;
- background: linear-gradient(135deg, #999 0%, #666 100%);
- }
- .btn-text {
- white-space: nowrap;
- }
- .tool-btn {
- background: transparent;
- color: white;
- width: 20px;
- height: 20px;
- font-size: 16px;
- &:hover {
- background: transparent;
- transform: none;
- }
- }
- .loading-spinner {
- width: 20px;
- height: 20px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top: 2px solid white;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- }
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- </style>
|