|
@@ -34,10 +34,10 @@
|
|
|
<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="上一页">
|
|
|
+ <button @click="previousSlide" :disabled="slideIndex === 0" title="上一页" v-if="!isFollowModeActive || props.type == '1'">
|
|
|
<IconLeftTwo class="control-icon" />
|
|
|
</button>
|
|
|
- <button @click="nextSlide" :disabled="slideIndex === slides.length - 1" title="下一页">
|
|
|
+ <button @click="nextSlide" :disabled="slideIndex === slides.length - 1" title="下一页" v-if="!isFollowModeActive || props.type == '1'">
|
|
|
<IconRightTwo class="control-icon" />
|
|
|
</button>
|
|
|
<!-- <button @click="resetZoom" title="重置缩放">
|
|
@@ -46,6 +46,14 @@
|
|
|
<button @click="enterFullscreen" title="全屏">
|
|
|
<IconFullScreenOne class="control-icon" />
|
|
|
</button>
|
|
|
+ <!-- 只有创建人才显示跟随模式按钮 -->
|
|
|
+ <button
|
|
|
+ v-if="isCreator"
|
|
|
+ @click="toggleFollowMode"
|
|
|
+ :class="{ 'follow-active': isFollowModeActive }"
|
|
|
+ >
|
|
|
+ {{ isFollowModeActive ? '关闭跟随模式' : '开启跟随模式' }}
|
|
|
+ </button>
|
|
|
<!-- <button @click="backToEditor" class="back-btn" title="返回编辑">
|
|
|
<IconEdit class="control-icon" />
|
|
|
</button> -->
|
|
@@ -127,10 +135,11 @@
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 收缩状态下的切换按钮 -->
|
|
|
- <div v-show="workPanelCollapsed" class="collapsed-tabs">
|
|
|
+ <!-- 侧边导航标签 - 无论展开还是收缩都显示在左侧 -->
|
|
|
+ <div class="side-nav-tabs">
|
|
|
<button
|
|
|
- class="collapsed-tab-btn"
|
|
|
+ v-if="currentSlideHasIframe"
|
|
|
+ class="side-nav-btn"
|
|
|
:class="{ active: rightPanelMode === 'homework' }"
|
|
|
@click="switchToHomework"
|
|
|
title="作业"
|
|
@@ -138,7 +147,7 @@
|
|
|
<img :src="rightPanelMode === 'homework' ? homeworkActiveIcon : homeworkIcon" alt="作业">
|
|
|
</button>
|
|
|
<button
|
|
|
- class="collapsed-tab-btn"
|
|
|
+ class="side-nav-btn"
|
|
|
:class="{ active: rightPanelMode === 'dialogue' }"
|
|
|
@click="switchToDialogue"
|
|
|
title="对话"
|
|
@@ -147,7 +156,7 @@
|
|
|
</button>
|
|
|
<button
|
|
|
v-if="isChoiceQuestion"
|
|
|
- class="collapsed-tab-btn"
|
|
|
+ class="side-nav-btn"
|
|
|
:class="{ active: rightPanelMode === 'choice' }"
|
|
|
@click="switchToChoice"
|
|
|
title="统计"
|
|
@@ -156,6 +165,8 @@
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
+
|
|
|
+
|
|
|
<!-- 作业区内容 -->
|
|
|
<div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
|
|
|
<div class="homework-title">已提交</div>
|
|
@@ -318,6 +329,10 @@ const workUpdateInterval = 5000 // 5秒更新一次
|
|
|
const courseDetail = ref<any>({})
|
|
|
const studentArray = ref<any>([])
|
|
|
|
|
|
+// 跟随模式相关状态
|
|
|
+const isCreator = ref(false) // 是否为创建人
|
|
|
+const isFollowModeActive = ref(false) // 跟随模式是否开启
|
|
|
+
|
|
|
// 计算未提交作业的学生
|
|
|
const unsubmittedStudents = computed(() => {
|
|
|
if (!studentArray.value || !workArray.value) return []
|
|
@@ -355,6 +370,26 @@ const switchToChoice = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 自动切换到可用的面板
|
|
|
+const autoSwitchToAvailablePanel = () => {
|
|
|
+ // 如果当前在作业区但没有iframe,自动切换到其他可用面板
|
|
|
+ if (rightPanelMode.value === 'homework' && !currentSlideHasIframe.value) {
|
|
|
+ if (isChoiceQuestion.value) {
|
|
|
+ rightPanelMode.value = 'choice'
|
|
|
+ console.log('自动切换到统计面板')
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ rightPanelMode.value = 'dialogue'
|
|
|
+ console.log('自动切换到对话面板')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 如果当前在统计面板但不是选择题,自动切换到对话面板
|
|
|
+ else if (rightPanelMode.value === 'choice' && !isChoiceQuestion.value) {
|
|
|
+ rightPanelMode.value = 'dialogue'
|
|
|
+ console.log('自动切换到对话面板')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 启动作业更新定时器
|
|
|
const startWorkTimer = () => {
|
|
|
if (workTimer.value) {
|
|
@@ -561,9 +596,21 @@ watch(() => slideIndex.value, (newIndex, oldIndex) => {
|
|
|
console.log('当前页面无iframe,停止作业更新定时器')
|
|
|
stopWorkTimer()
|
|
|
}
|
|
|
+ if (isFollowModeActive.value && isCreator.value) {
|
|
|
+ api.updateCourseFollowC(newIndex, props.courseid as string)
|
|
|
+ }
|
|
|
+ // 自动切换到可用的面板
|
|
|
+ autoSwitchToAvailablePanel()
|
|
|
}
|
|
|
}, { immediate: false, deep: false })
|
|
|
|
|
|
+// 监听iframe状态变化,自动切换面板
|
|
|
+watch(() => currentSlideHasIframe.value, (hasIframe) => {
|
|
|
+ if (!hasIframe) {
|
|
|
+ autoSwitchToAvailablePanel()
|
|
|
+ }
|
|
|
+}, { immediate: false })
|
|
|
+
|
|
|
// 全屏
|
|
|
const enterFullscreen = () => {
|
|
|
if (document.fullscreenElement) {
|
|
@@ -782,6 +829,7 @@ const importJSON = (jsonData: any) => {
|
|
|
getWork()
|
|
|
startWorkTimer()
|
|
|
}
|
|
|
+ selectCourseSLook()
|
|
|
console.log('组件重新渲染完成')
|
|
|
}, 500)
|
|
|
})
|
|
@@ -1161,6 +1209,7 @@ const getCourseDetail = async () => {
|
|
|
const courseData = res[0][0]
|
|
|
courseDetail.value = courseData
|
|
|
selectWorksStudent()
|
|
|
+ checkIsCreator()
|
|
|
const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
|
|
|
console.log(pptJSONUrl)
|
|
|
|
|
@@ -1291,6 +1340,60 @@ const checkWorkArrayChanged = (oldArray: WorkItem[], newArray: WorkItem[]): bool
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
+// 查询课程跟随状态
|
|
|
+const selectCourseSLook = async () => {
|
|
|
+ const res = await api.selectCourseSLook(props.courseid as string)
|
|
|
+ console.log('selectCourseSLook', res)
|
|
|
+ if (res[0][0].follow == 2) {
|
|
|
+ if (props.type == '2') {
|
|
|
+ goToSlide(Number(res[0][0].followC))
|
|
|
+ }
|
|
|
+ isFollowModeActive.value = true
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ isFollowModeActive.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 切换跟随模式
|
|
|
+const toggleFollowMode = async () => {
|
|
|
+ try {
|
|
|
+ const newFollowState = !isFollowModeActive.value
|
|
|
+ const sopen = newFollowState ? 2 : 1
|
|
|
+
|
|
|
+ // 调用API更新跟随状态
|
|
|
+ const res = await api.updateCourseFollow(sopen, props.courseid as string)
|
|
|
+ console.log('更新跟随模式状态:', res)
|
|
|
+
|
|
|
+ if (res) {
|
|
|
+ isFollowModeActive.value = newFollowState
|
|
|
+ message.success(newFollowState ? '跟随模式已开启' : '跟随模式已关闭')
|
|
|
+
|
|
|
+ // 如果开启跟随模式,设置当前幻灯片为跟随目标
|
|
|
+ if (newFollowState) {
|
|
|
+ await api.updateCourseFollowC(slideIndex.value, props.courseid as string)
|
|
|
+ console.log('设置当前幻灯片为跟随目标:', slideIndex.value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ message.error('操作失败,请重试')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('切换跟随模式失败:', error)
|
|
|
+ message.error('操作失败,请重试')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 检查是否为创建人
|
|
|
+const checkIsCreator = () => {
|
|
|
+ // 这里可以根据实际业务逻辑判断是否为创建人
|
|
|
+ // 比如通过props中的userid与课程创建者ID比较
|
|
|
+ if (courseDetail.value && props.userid) {
|
|
|
+ isCreator.value = courseDetail.value.userid === props.userid
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
document.addEventListener('keydown', handleKeydown)
|
|
|
|
|
@@ -1319,6 +1422,9 @@ onMounted(() => {
|
|
|
|
|
|
// 处理iframe链接
|
|
|
processIframeLinks()
|
|
|
+
|
|
|
+ // 初始化时检查并自动切换到可用面板
|
|
|
+ autoSwitchToAvailablePanel()
|
|
|
})
|
|
|
|
|
|
// 监听窗口大小变化
|
|
@@ -1433,6 +1539,12 @@ onUnmounted(() => {
|
|
|
border-left: 1px solid #e0e0e0;
|
|
|
overflow-y: auto;
|
|
|
transition: width .2s ease;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-content {
|
|
|
+ margin-right: 48px;
|
|
|
+ padding: 0 8px;
|
|
|
}
|
|
|
.layout-content-right.collapsed {
|
|
|
width: 48px;
|
|
@@ -1443,6 +1555,71 @@ onUnmounted(() => {
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
|
|
|
+/* 侧边导航标签样式 */
|
|
|
+.side-nav-tabs {
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ top: 60px;
|
|
|
+ bottom: 0;
|
|
|
+ width: 48px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 0;
|
|
|
+ align-items: center;
|
|
|
+ background: #fff;
|
|
|
+ border-left: 1px solid #e0e0e0;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.side-nav-btn {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ min-height: 40px;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 8px;
|
|
|
+ gap: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #1890ff;
|
|
|
+ transform: scale(1.02);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ border-color: #3681fc;
|
|
|
+ background: #3681fc;
|
|
|
+ box-shadow: 0 0 0 2px rgba(54, 129, 252, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ img {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ object-fit: contain;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #333;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active span {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/* 收缩时头部仅显示按钮,并保持按钮在可用宽度内水平居中 */
|
|
|
.layout-content-right.collapsed .right-panel-header {
|
|
|
justify-content: center;
|
|
@@ -1676,7 +1853,7 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
- padding: 0 24px;
|
|
|
+ padding: 0 8px;
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
|
&.hidden {
|
|
@@ -1728,6 +1905,18 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ &.follow-active {
|
|
|
+ background-color: #3681fc;
|
|
|
+ color: #fff !important;
|
|
|
+ border-color: #3681fc;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #2d6fd9;
|
|
|
+ border-color: #2d6fd9;
|
|
|
+ color: #fff !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.control-icon {
|
|
|
font-size: 16px;
|
|
|
}
|