|
@@ -8,24 +8,24 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- 左侧导航栏 -->
|
|
|
- <div class="layout-content-left" v-show="type == '1'">
|
|
|
+ <div class="layout-content-left" v-show="type == '1'" :class="{ collapsed: slidePanelCollapsed }">
|
|
|
<div class="thumbnails">
|
|
|
- <div class="viewer-header">
|
|
|
- <h3>幻灯片导航</h3>
|
|
|
+ <div class="viewer-header slide-header">
|
|
|
+ <h3 v-show="!slidePanelCollapsed">幻灯片导航</h3>
|
|
|
+ <button class="collapse-btn" @click="slidePanelCollapsed = !slidePanelCollapsed" :title="slidePanelCollapsed ? '展开' : '收起'">
|
|
|
+ <span v-if="slidePanelCollapsed">›</span>
|
|
|
+ <span v-else>‹</span>
|
|
|
+ </button>
|
|
|
</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 v-show="!slidePanelCollapsed">
|
|
|
+ <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>
|
|
|
-
|
|
|
- <!-- <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>
|
|
|
|
|
@@ -116,14 +116,48 @@
|
|
|
</div>
|
|
|
<div class="layout-content-right" v-show="type == '1'" :class="{ collapsed: workPanelCollapsed }">
|
|
|
<div class="thumbnails">
|
|
|
- <div class="viewer-header homework-header">
|
|
|
- <h3 v-show="!workPanelCollapsed">作业区</h3>
|
|
|
+ <div class="viewer-header right-panel-header">
|
|
|
+ <h3 v-show="!workPanelCollapsed">{{
|
|
|
+ rightPanelMode === 'homework' ? '作业区' :
|
|
|
+ rightPanelMode === 'dialogue' ? '对话区' : '统计'
|
|
|
+ }}</h3>
|
|
|
<button class="collapse-btn" @click="workPanelCollapsed = !workPanelCollapsed" :title="workPanelCollapsed ? '展开' : '收起'">
|
|
|
<span v-if="workPanelCollapsed">›</span>
|
|
|
<span v-else>‹</span>
|
|
|
</button>
|
|
|
</div>
|
|
|
- <div v-show="!workPanelCollapsed">
|
|
|
+
|
|
|
+ <!-- 收缩状态下的切换按钮 -->
|
|
|
+ <div v-show="workPanelCollapsed" class="collapsed-tabs">
|
|
|
+ <button
|
|
|
+ class="collapsed-tab-btn"
|
|
|
+ :class="{ active: rightPanelMode === 'homework' }"
|
|
|
+ @click="switchToHomework"
|
|
|
+ title="作业"
|
|
|
+ >
|
|
|
+ <img :src="rightPanelMode === 'homework' ? homeworkActiveIcon : homeworkIcon" alt="作业">
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="collapsed-tab-btn"
|
|
|
+ :class="{ active: rightPanelMode === 'dialogue' }"
|
|
|
+ @click="switchToDialogue"
|
|
|
+ title="对话"
|
|
|
+ >
|
|
|
+ <img :src="rightPanelMode === 'dialogue' ? dialogueActiveIcon : dialogueIcon" alt="对话">
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="isChoiceQuestion"
|
|
|
+ class="collapsed-tab-btn"
|
|
|
+ :class="{ active: rightPanelMode === 'choice' }"
|
|
|
+ @click="switchToChoice"
|
|
|
+ title="统计"
|
|
|
+ >
|
|
|
+ <img :src="rightPanelMode === 'choice' ? choiceActiveIcon : choiceIcon" alt="统计">
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 作业区内容 -->
|
|
|
+ <div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
|
|
|
<div class="homework-title">已提交</div>
|
|
|
<div v-if="workLoading" class="homework-loading">正在加载作业...</div>
|
|
|
<div v-else>
|
|
@@ -136,6 +170,29 @@
|
|
|
暂无作业提交
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 未提交作业的学生列表 -->
|
|
|
+ <div v-if="unsubmittedStudents && unsubmittedStudents.length > 0" class="homework-title" style="margin-top: 20px;">未提交</div>
|
|
|
+ <div v-if="unsubmittedStudents && unsubmittedStudents.length > 0">
|
|
|
+ <div v-if="studentLoading" class="homework-loading">正在加载学生信息...</div>
|
|
|
+ <div v-else>
|
|
|
+ <div class="homework-grid">
|
|
|
+ <button class="homework-btn unsubmitted" v-for="(student, idx) in unsubmittedStudents" :key="student.id ?? idx" :title="student.name" disabled>
|
|
|
+ <span class="homework-btn__text">{{ student.name }}</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 对话区内容 -->
|
|
|
+ <div v-show="!workPanelCollapsed && rightPanelMode === 'dialogue'" class="panel-content">
|
|
|
+ <DialoguePanel />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 选择题统计内容 -->
|
|
|
+ <div v-show="!workPanelCollapsed && rightPanelMode === 'choice'" class="panel-content">
|
|
|
+ <ChoiceStatistics :workArray="workArray" :elementList="elementList" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -167,6 +224,16 @@ import ShotWorkModal from './components/ShotWorkModal.vue'
|
|
|
import QAWorkModal from './components/QAWorkModal.vue'
|
|
|
import ChoiceWorkModal from './components/ChoiceWorkModal.vue'
|
|
|
import AIWorkModal from './components/AIWorkModal.vue'
|
|
|
+import DialoguePanel from './components/DialoguePanel.vue'
|
|
|
+import ChoiceStatistics from './components/ChoiceStatistics.vue'
|
|
|
+
|
|
|
+// 导入图片资源
|
|
|
+import homeworkIcon from '@/assets/img/homework.png'
|
|
|
+import homeworkActiveIcon from '@/assets/img/homework-active.png'
|
|
|
+import dialogueIcon from '@/assets/img/dialogue.png'
|
|
|
+import dialogueActiveIcon from '@/assets/img/dialogue-active.png'
|
|
|
+import choiceIcon from '@/assets/img/choice.png'
|
|
|
+import choiceActiveIcon from '@/assets/img/choice-active.png'
|
|
|
|
|
|
// 定义组件props
|
|
|
interface Props {
|
|
@@ -219,6 +286,7 @@ const slideHeight = ref(0)
|
|
|
// 添加loading状态
|
|
|
const isLoading = ref(false)
|
|
|
const workLoading = ref(false)
|
|
|
+const studentLoading = ref(false)
|
|
|
|
|
|
// 作业数组
|
|
|
type WorkItem = {
|
|
@@ -238,11 +306,55 @@ const visibleAI = ref(false)
|
|
|
|
|
|
// 作业区收缩状态
|
|
|
const workPanelCollapsed = ref(false)
|
|
|
+// 幻灯片导航收缩状态
|
|
|
+const slidePanelCollapsed = ref(false)
|
|
|
+// 右侧面板当前显示的内容:'homework' | 'dialogue' | 'choice'
|
|
|
+const rightPanelMode = ref<'homework' | 'dialogue' | 'choice'>('homework')
|
|
|
|
|
|
// 定时器相关
|
|
|
const workTimer = ref<number | null>(null)
|
|
|
const workUpdateInterval = 5000 // 5秒更新一次
|
|
|
|
|
|
+const courseDetail = ref<any>({})
|
|
|
+const studentArray = ref<any>([])
|
|
|
+
|
|
|
+// 计算未提交作业的学生
|
|
|
+const unsubmittedStudents = computed(() => {
|
|
|
+ if (!studentArray.value || !workArray.value) return []
|
|
|
+
|
|
|
+ // 获取已提交作业的学生姓名
|
|
|
+ const submittedNames = workArray.value.map(work => work.name)
|
|
|
+
|
|
|
+ // 过滤出未提交作业的学生
|
|
|
+ return studentArray.value.filter((student: any) => !submittedNames.includes(student.name))
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 切换到作业区
|
|
|
+const switchToHomework = () => {
|
|
|
+ rightPanelMode.value = 'homework'
|
|
|
+ if (workPanelCollapsed.value) {
|
|
|
+ workPanelCollapsed.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 切换到对话区
|
|
|
+const switchToDialogue = () => {
|
|
|
+ rightPanelMode.value = 'dialogue'
|
|
|
+ if (workPanelCollapsed.value) {
|
|
|
+ workPanelCollapsed.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 切换到选择题统计
|
|
|
+const switchToChoice = () => {
|
|
|
+ rightPanelMode.value = 'choice'
|
|
|
+ if (workPanelCollapsed.value) {
|
|
|
+ workPanelCollapsed.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 启动作业更新定时器
|
|
|
const startWorkTimer = () => {
|
|
|
if (workTimer.value) {
|
|
@@ -265,14 +377,14 @@ const stopWorkTimer = () => {
|
|
|
}
|
|
|
|
|
|
// 收缩/展开后重新计算中间画布尺寸(在 DOM 更新并完成过渡后)
|
|
|
-watch(() => workPanelCollapsed.value, async () => {
|
|
|
+watch([() => workPanelCollapsed.value, () => slidePanelCollapsed.value], async () => {
|
|
|
// 等待本次 DOM 更新
|
|
|
await nextTick()
|
|
|
// 先在下一帧计算一次,确保初步布局就绪
|
|
|
requestAnimationFrame(() => {
|
|
|
calculateScale()
|
|
|
})
|
|
|
- // 再在过渡结束后(与右栏 width .2s 过渡一致)复算一次,确保最终尺寸
|
|
|
+ // 再在过渡结束后(与左右栏 width .2s 过渡一致)复算一次,确保最终尺寸
|
|
|
setTimeout(() => {
|
|
|
calculateScale()
|
|
|
}, 220)
|
|
@@ -388,6 +500,12 @@ const elementList = computed(() => {
|
|
|
return currentSlide.value?.elements || []
|
|
|
})
|
|
|
|
|
|
+// 检查当前是否为选择题(toolType为45)
|
|
|
+const isChoiceQuestion = computed(() => {
|
|
|
+ const frame = elementList.value.find(element => element.type === ElementTypes.FRAME)
|
|
|
+ return frame?.toolType === 45
|
|
|
+})
|
|
|
+
|
|
|
// 检测当前幻灯片是否包含iframe元素
|
|
|
const currentSlideHasIframe = computed(() => {
|
|
|
console.log('elementList.value', elementList.value)
|
|
@@ -1040,8 +1158,10 @@ const getCourseDetail = async () => {
|
|
|
try {
|
|
|
const res = await api.getCourseDetail(props.courseid as string)
|
|
|
console.log(res)
|
|
|
- const courseDetail = res[0][0]
|
|
|
- const pptJSONUrl = JSON.parse(courseDetail.chapters).pptData ? JSON.parse(courseDetail.chapters).pptData : ''
|
|
|
+ const courseData = res[0][0]
|
|
|
+ courseDetail.value = courseData
|
|
|
+ selectWorksStudent()
|
|
|
+ const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
|
|
|
console.log(pptJSONUrl)
|
|
|
|
|
|
if (pptJSONUrl) {
|
|
@@ -1131,6 +1251,29 @@ const getWork = async (isUpdate = false) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const selectWorksStudent = async () => {
|
|
|
+ studentLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await api.selectWorksStudent(props.oid as string, courseDetail.value.juri as string)
|
|
|
+ console.log('selectWorksStudent', res)
|
|
|
+ const students = res[0]
|
|
|
+ console.log('students', students)
|
|
|
+ if (props.cid) {
|
|
|
+ studentArray.value = students.filter((student: any) => student.classid.includes(props.cid))
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ studentArray.value = students
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('获取学生信息失败:', error)
|
|
|
+ message.error('获取学生信息失败')
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ studentLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 检查作业数组是否发生变化
|
|
|
const checkWorkArrayChanged = (oldArray: WorkItem[], newArray: WorkItem[]): boolean => {
|
|
|
if (oldArray.length !== newArray.length) return true
|
|
@@ -1170,7 +1313,6 @@ onMounted(() => {
|
|
|
getCourseDetail()
|
|
|
|
|
|
|
|
|
-
|
|
|
// 计算初始缩放比例
|
|
|
nextTick(() => {
|
|
|
calculateScale()
|
|
@@ -1268,6 +1410,20 @@ onUnmounted(() => {
|
|
|
background-color: #fff;
|
|
|
border-right: 1px solid #e0e0e0;
|
|
|
overflow-y: auto;
|
|
|
+ transition: width .2s ease;
|
|
|
+}
|
|
|
+.layout-content-left.collapsed {
|
|
|
+ width: 48px;
|
|
|
+}
|
|
|
+.slide-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+/* 收缩时头部仅显示按钮,并保持按钮在可用宽度内水平居中 */
|
|
|
+.layout-content-left.collapsed .slide-header {
|
|
|
+ justify-content: center;
|
|
|
+ padding: 8px;
|
|
|
}
|
|
|
|
|
|
.layout-content-right {
|
|
@@ -1281,16 +1437,63 @@ onUnmounted(() => {
|
|
|
.layout-content-right.collapsed {
|
|
|
width: 48px;
|
|
|
}
|
|
|
-.homework-header {
|
|
|
+.right-panel-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
+
|
|
|
/* 收缩时头部仅显示按钮,并保持按钮在可用宽度内水平居中 */
|
|
|
-.layout-content-right.collapsed .homework-header {
|
|
|
+.layout-content-right.collapsed .right-panel-header {
|
|
|
justify-content: center;
|
|
|
padding: 8px;
|
|
|
}
|
|
|
+
|
|
|
+/* 收缩状态下的切换按钮 */
|
|
|
+.collapsed-tabs {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ // padding: 8px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.collapsed-tab-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #1890ff;
|
|
|
+ transform: scale(1.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.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;
|
|
|
+ transition: transform 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover img {
|
|
|
+ transform: scale(1.1);
|
|
|
+ }
|
|
|
+}
|
|
|
.collapse-btn {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
@@ -1349,6 +1552,18 @@ onUnmounted(() => {
|
|
|
.homework-btn:hover {
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
|
|
}
|
|
|
+
|
|
|
+.homework-btn.unsubmitted {
|
|
|
+ border-color: #d9d9d9;
|
|
|
+ color: #999;
|
|
|
+ background: #f5f5f5;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.homework-btn.unsubmitted:hover {
|
|
|
+ box-shadow: none;
|
|
|
+ transform: none;
|
|
|
+}
|
|
|
.homework-loading {
|
|
|
padding: 12px;
|
|
|
color: #666;
|
|
@@ -1360,6 +1575,8 @@ onUnmounted(() => {
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+
|
|
|
.thumbnails {
|
|
|
padding: 0;
|
|
|
|