SanHQin 1 ماه پیش
والد
کامیت
fd5869f5b8

BIN
src/assets/img/close.png


+ 220 - 325
src/views/Student/components/answerTheResult.vue

@@ -1,7 +1,7 @@
 <template>
 	<div class="answerTheResult">
 		<div class="atr_detail">
-			<div class="atr_d_btn">查看详细</div>
+			<div class="atr_d_btn" @click="lookDetail()">查看详细</div>
 			<div class="atr_d_msg">
 				<div>参与人数</div>
 				<span
@@ -13,7 +13,7 @@
 
 			<div class="atr_d_msg" v-if="workDetail && workDetail.type === '45'">
 				<div>正确率</div>
-				<span>{{choiceQuestionList[workIndex].accuracyRate}}%({{choiceQuestionList[workIndex].yes}}/{{choiceQuestionList[workIndex].all}})</span>
+				<span v-if="choiceQuestionListData[workIndex]">{{choiceQuestionListData[workIndex].accuracyRate}}%({{choiceQuestionListData[workIndex].yes}}/{{choiceQuestionListData[workIndex].all}})</span>
 			</div>
 
 			<div class="atr_d_msg" v-if="choiceQuestionAnswer">
@@ -21,9 +21,9 @@
 				<span style="color: #03ae2b">{{ choiceQuestionAnswer }}</span>
 			</div>
 
-			<span class="atr_d_line"></span>
+			<span class="atr_d_line" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0"></span>
 
-			<div class="no_submit">
+			<div class="no_submit" v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0">
 				<div>未提交人员</div>
 				<img
 					@click="showNoSubmitDetail = !showNoSubmitDetail"
@@ -34,7 +34,7 @@
 
 			<div
 				class="no_submitList"
-				v-if="props.unsubmittedStudents?.length > 0"
+				v-if="props.unsubmittedStudents && props.unsubmittedStudents?.length > 0"
 				:class="{ no_submitList_active: showNoSubmitDetail }"
 			>
 				<div
@@ -51,18 +51,19 @@
 			v-if="
 				workDetail &&
 				workDetail.type === '45' &&
-				choiceQuestionList[workIndex] &&
-				choiceQuestionList[workIndex].choiceUser
+				choiceQuestionListData[workIndex] &&
+				choiceQuestionListData[workIndex].choiceUser
 			"
 		>
 
 		<div class="atr_t45a_title">
-			<span>{{ workIndex+1 }}、{{ choiceQuestionList[workIndex].type=='1'?'单选题':'多选题' }}:</span>
-			<span>{{ choiceQuestionList[workIndex].teststitle }}</span>
+			<span>{{ workIndex+1 }}、{{ choiceQuestionListData[workIndex].type=='1'?'单选题':'多选题' }}:</span>
+			<span>{{ choiceQuestionListData[workIndex].teststitle }}</span>
+      <img @click="previewImage(choiceQuestionListData[workIndex].timuList[0].src)" :src="choiceQuestionListData[workIndex].timuList[0].src" v-if="choiceQuestionListData[workIndex].timuList.length>0">
 		</div>
 
 			<template
-				v-for="(op, idx) in choiceQuestionList[workIndex].choiceUser"
+				v-for="(op, idx) in choiceQuestionListData[workIndex].choiceUser"
 				:key="`${workIndex}_${idx}`"
 			>
 				<div class="atr_t45a_item">
@@ -81,7 +82,7 @@
 							<span v-if="op.isAnswer">正确</span>
 						</div>
 						<img
-							@click="op.show = !op.show"
+							@click="changeShow(op, idx)"
 							:class="{ show_active: !op.show }"
 							src="../../../assets/img/arrow_up.png"
 						/>
@@ -101,384 +102,270 @@
 				</div>
 			</template>
 
-			<div class="nextAndUpBtn">
+			<div class="nextAndUpBtn" v-if="choiceQuestionListData.length>1">
 				<span :class="{no_active:workIndex==0}" @click="changeWorkIndex(0)">上一题</span>
-				<span :class="{no_active:choiceQuestionList.length-1<=workIndex}"  @click="changeWorkIndex(1)">下一题</span>
+				<span :class="{no_active:choiceQuestionListData.length-1<=workIndex}"  @click="changeWorkIndex(1)">下一题</span>
 			</div>
 		</div>
 	</div>
 
 	<!-- 预览放大(带缩放/拖拽/旋转/工具栏) -->
-  <Teleport to="body">
-    <div v-if="previewVisible" class="image-preview" @click.self="closePreview" @wheel.prevent="onWheel">
-      <div class="image-preview__toolbar">
-        <button @click.stop="zoomOut">-</button>
-        <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">重置</button>
-        <button @click.stop="rotateLeft">⟲</button>
-        <button @click.stop="rotateRight">⟳</button>
-        <button @click.stop="toggleFit">{{ fitMode ? '实际大小' : '适应屏幕' }}</button>
-        <button @click.stop="closePreview">关闭</button>
-      </div>
-      <div class="image-preview__stage"
-           @mousedown="onDragStart"
-           @mousemove="onDragMove"
-           @mouseup="onDragEnd"
-           @mouseleave="onDragEnd"
-           @dblclick.stop="toggleZoom">
-        <!-- 预览中的 Loading 状态 -->
-        <div v-if="previewImageLoading" class="preview-loading">
-          <div class="loading-spinner"></div>
-          <div class="loading-text">图片加载中...</div>
-        </div>
-        
-        <img 
-          v-show="!previewImageLoading"
-          :src="imageUrl" 
-          alt="预览" 
-          class="image-preview__img"
-          :style="imgStyle" 
-          draggable="false"
-          @load="onPreviewImageLoad"
-          @error="onPreviewImageError"
-        />
-      </div>
-    </div>
-  </Teleport>
+  <previewImageTool ref="previewImageToolRef"/>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, watch } from "vue";
-import api from "../../../services/course";
-
+import { ref, computed, watch, defineExpose } from 'vue'
+import previewImageTool from '@/views/components/tool/previewImageTool.vue'
+import api from '../../../services/course'
 interface Props {
 	workArray?: object[] | null;
 	unsubmittedStudents?: object[] | null;
 	workId?: string | null;
+  slideIndex?:number
 }
 
 const props = withDefaults(defineProps<Props>(), {
-	workArray: () => [],
-	unsubmittedStudents: () => [],
-	workId: "",
-});
+  workArray: () => [],
+  unsubmittedStudents: () => [],
+  workId: '',
+  slideIndex: 0
+})
+
+const emit = defineEmits<{
+  (e: 'openChoiceQuestionDetail', v: number): void
+}>()
 
 
 
 
 // 已提交的作业数量
 const workArrayLength = computed(() => {
-	let _result = 0;
-	if (props.workArray) {
-		_result = props.workArray.length;
-	}
-	return _result;
-});
+  let _result = 0
+  if (props.workArray) {
+    _result = props.workArray.length
+  }
+  return _result
+})
 // 未提交的作业数量
 const unsubmittedStudentsLength = computed(() => {
-	let _result = 0;
-	if (props.unsubmittedStudents) {
-		_result = props.unsubmittedStudents.length;
-	}
-	return _result;
-});
+  let _result = 0
+  if (props.unsubmittedStudents) {
+    _result = props.unsubmittedStudents.length
+  }
+  return _result
+})
 
 // 选项序号
 const serialNumber = ref<string[]>([
-	"A",
-	"B",
-	"C",
-	"D",
-	"E",
-	"F",
-	"G",
-	"H",
-	"I",
-	"J",
-	"K",
-	"L",
-	"M",
-	"N",
-	"O",
-	"P",
-	"Q",
-	"R",
-	"S",
-	"T",
-	"U",
-	"V",
-	"W",
-	"X",
-	"Y",
-	"Z",
-]);
+  'A',
+  'B',
+  'C',
+  'D',
+  'E',
+  'F',
+  'G',
+  'H',
+  'I',
+  'J',
+  'K',
+  'L',
+  'M',
+  'N',
+  'O',
+  'P',
+  'Q',
+  'R',
+  'S',
+  'T',
+  'U',
+  'V',
+  'W',
+  'X',
+  'Y',
+  'Z',
+])
 
 // 第几题
-const workIndex = ref<number>(0);
+const workIndex = ref<number>(0)
 
 // 是否显示未提交人员
-const showNoSubmitDetail = ref<boolean>(false);
+const showNoSubmitDetail = ref<boolean>(false)
 
 // 作业详细
-const workDetail = ref<any>({});
+const workDetail = ref<any>({})
 
 // 获取作业详细
 const getWorkDetail = async () => {
-	if (props.workId) {
-		const _res = await api.getWorkDetail({ id: props.workId });
-		const _data = _res[0][0];
-		if (_data) {
-			_data.json = JSON.parse(_data.json);
-			workDetail.value = _data;
-		}
-	}
-};
+  if (props.workId) {
+    const _res = await api.getWorkDetail({ id: props.workId })
+    const _data = _res[0][0]
+    if (_data) {
+      _data.json = JSON.parse(_data.json)
+      workDetail.value = _data
+      choiceQuestionListData.value = choiceQuestionList()
+    }
+  }
+}
 
 // 选择题题目数组
-const choiceQuestionList = computed(() => {
-	let _result: any[] = [];
-	if (workDetail.value && workDetail.value.type == "45") {
-		let _workData = workDetail.value.json.testJson;
-		_workData.forEach((item: any, index: number) => {
-			// 修复 props.workArray 可能为 null 的问题
-			item.choiceUser = [];
-			item.yes = 0;
-			item.no = 0;
-			item.checkList.forEach((op: any, oidx: number) =>
-				item.choiceUser.push({
-					option: op,
-					user: [],
-					show: false,
-					isAnswer: Array.isArray(item.answer)
-						? item.answer.includes(oidx)
-						: item.answer === oidx,
-				})
-			);
-		});
-		(props.workArray ?? []).forEach(
-			(studentWork: any, studentIndex: number) => {
-				let _studentContent: any = JSON.parse(
-					decodeURIComponent(studentWork.content)
-				).testJson;
-				_studentContent.forEach((test: any, testIndex: number) => {
-					if (test.userAnswer || test.userAnswer == 0) {
-						if (Array.isArray(test.userAnswer)) {
-							test.userAnswer.forEach((ch: number) => {
-								_workData[testIndex].choiceUser[ch].user.push(studentWork.name);
-							});
-							if (
-								JSON.stringify(
-									test.userAnswer.sort((a: number, b: number) => a - b)
-								) == JSON.stringify(_workData[testIndex].answer)
-							) {
-								_workData[testIndex].yes += 1;
-							} else {
-								_workData[testIndex].no += 1;
-							}
-						} else {
-							_workData[testIndex].choiceUser[test.userAnswer].user.push(
-								studentWork.name
-							);
-							if (test.userAnswer == _workData[testIndex].answer) {
-								_workData[testIndex].yes += 1;
-							} else {
-								_workData[testIndex].no += 1;
-							}
-						}
-					}
-				});
-			}
-		);
-
-		_workData.forEach((item:any)=>{
-			item.all = item.yes + item.no;
-			if (item.all === 0) {
-				item.accuracyRate = 0;
-			} else {
-				let rate = (item.yes / item.all) * 100;
-				item.accuracyRate = Number.isInteger(rate) ? rate : Number(rate.toFixed(2));
-			}
-		})
+const choiceQuestionList = () => {
+  let _result: any[] = []
+  if (workDetail.value && workDetail.value.type === '45') {
+    const _workData = workDetail.value.json.testJson
+    _workData.forEach((item: any, index: number) => {
+      // 修复 props.workArray 可能为 null 的问题
+      item.choiceUser = []
+      item.yes = 0
+      item.no = 0
+      item.checkList.forEach((op: any, oidx: number) =>
+        item.choiceUser.push({
+          index: serialNumber.value[oidx],
+          option: op,
+          user: [],
+          show: false,
+          isAnswer: Array.isArray(item.answer)
+            ? item.answer.includes(oidx)
+            : item.answer === oidx,
+        })
+      )
+    });
+    (props.workArray ?? []).forEach(
+      (studentWork: any, studentIndex: number) => {
+        const _studentContent: any = JSON.parse(
+          decodeURIComponent(studentWork.content)
+        ).testJson
+        _studentContent.forEach((test: any, testIndex: number) => {
+          if (test.userAnswer || test.userAnswer === 0) {
+            if (Array.isArray(test.userAnswer)) {
+              test.userAnswer.forEach((ch: number) => {
+                _workData[testIndex].choiceUser[ch].user.push(studentWork.name)
+              })
+              if (
+                JSON.stringify(
+                  test.userAnswer.sort((a: number, b: number) => a - b)
+                ) === JSON.stringify(_workData[testIndex].answer)
+              ) {
+                _workData[testIndex].yes += 1
+              }
+              else {
+                _workData[testIndex].no += 1
+              }
+            }
+            else {
+              _workData[testIndex].choiceUser[test.userAnswer].user.push(
+                studentWork.name
+              )
+              if (test.userAnswer === _workData[testIndex].answer) {
+                _workData[testIndex].yes += 1
+              }
+              else {
+                _workData[testIndex].no += 1
+              }
+            }
+          }
+        })
+      }
+    )
+
+    _workData.forEach((item:any) => {
+      item.all = item.yes + item.no
+      if (item.all === 0) {
+        item.accuracyRate = 0
+      }
+      else {
+        const rate = (item.yes / item.all) * 100
+        item.accuracyRate = Number.isInteger(rate) ? rate : Number(rate.toFixed(2))
+      }
+    })
+
+    _result = _workData
+  }
 
-		_result = _workData;
-	}
+  return _result
+}
 
-	return _result;
-});
+
+const choiceQuestionListData = ref<any[]>([])
 
 // 选择题题目正确答案
 const choiceQuestionAnswer = computed(() => {
-	let _result = "";
-	if (
-		workDetail.value &&
-		workDetail.value.type == "45" &&
+  let _result = ''
+  if (
+    workDetail.value &&
+		workDetail.value.type === '45' &&
 		workDetail.value.json.testJson[workIndex.value]
-	) {
-		let _answer = workDetail.value.json.testJson[workIndex.value].answer;
-		if (Array.isArray(_answer)) {
-			_result = _answer.map((i: any) => serialNumber.value[i]).join("、");
-		} else {
-			_result = serialNumber.value[_answer];
-		}
-	}
+  ) {
+    const _answer = workDetail.value.json.testJson[workIndex.value].answer
+    if (Array.isArray(_answer)) {
+      _result = _answer.map((i: any) => serialNumber.value[i]).join('、')
+    }
+    else {
+      _result = [serialNumber.value[_answer]].join('、')
+    }
+  }
+
+  return _result
+})
 
-	return _result;
-});
 
 // //当前题目正确率
 // const choiceQuestionAccuracyRate = computed
 
 // 监听作业Id
 watch(
-	() => props.workId,
-	(newVal, oldVal) => {
-		console.log("props.workId变化", { newVal, oldVal });
-		if (newVal && newVal !== oldVal) {
-			getWorkDetail();
-		}
-	},
-	{ immediate: true }
-);
-
-//切换题目
-const changeWorkIndex = (type:number) =>{
-	if(type===0){
-		if(workIndex.value==0)return;
-		workIndex.value-=1;
-	}else if(type===1){
-		if(choiceQuestionList.value.length-1<=workIndex.value)return
-		workIndex.value+=1;
-	}
-}
-
-
-
-const imageUrl = ref<string>('')
-// Loading 状态
-const imageLoading = ref(false)
-const previewImageLoading = ref(false)
-
-const previewImage = (url:string)=>{
-	imageUrl.value = url;
-	previewVisible.value = true
-}
-
-// 监听图片URL变化,重置loading状态
-watch(imageUrl, (newUrl) => {
-  if (newUrl) {
-    imageLoading.value = true
-    previewImageLoading.value = true
+  () => props.workId,
+  (newVal, oldVal) => {
+    console.log('props.workId变化', { newVal, oldVal })
+    if (newVal && newVal !== oldVal) {
+      getWorkDetail()
+    }
+  },
+  { immediate: true }
+)
+
+
+// 监听已提交的作业
+watch(() => props.workArray, (newVal, oldVal) => {
+  if (newVal && newVal !== oldVal) {
+    choiceQuestionListData.value = choiceQuestionList()
+    // choiceQuestionAnswerData.value = choiceQuestionAnswer()
   }
-})
+}, { immediate: true })
 
-const previewVisible = ref(false)
-const scale = ref(1)
-const rotate = ref(0)
-const offsetX = ref(0)
-const offsetY = ref(0)
-const dragging = ref(false)
-const lastX = ref(0)
-const lastY = ref(0)
-const fitMode = ref(true)
-
-const imgStyle = computed(() => {
-  const t = `translate(${offsetX.value}px, ${offsetY.value}px) rotate(${rotate.value}deg) scale(${scale.value})`
-  return {
-    transform: t
+// 切换题目
+const changeWorkIndex = (type:number) => {
+  if (type === 0) {
+    if (workIndex.value === 0) return
+    workIndex.value -= 1
+  }
+  else if (type === 1) {
+    if (choiceQuestionListData.value.length - 1 <= workIndex.value) return
+    workIndex.value += 1
   }
-})
-
-// 图片加载事件处理
-const onImageLoad = () => {
-  imageLoading.value = false
-}
-
-const onImageError = () => {
-  imageLoading.value = false
-  console.error('图片加载失败')
-}
-
-const onPreviewImageLoad = () => {
-  previewImageLoading.value = false
-}
-
-const onPreviewImageError = () => {
-  previewImageLoading.value = false
-  console.error('预览图片加载失败')
-}
-
-const openPreview = () => {
-  if (imageLoading.value) return // 如果主图片还在加载,不允许打开预览
-  previewVisible.value = true
-  previewImageLoading.value = true // 预览时重新显示loading
-  nextTickFit()
 }
 
-const closePreview = () => {
-  previewVisible.value = false
+// 切换题目展示所有人选项
+const changeShow = (op: any, idx: number) => {
+  choiceQuestionListData.value[workIndex.value].choiceUser[idx].show = !choiceQuestionListData.value[workIndex.value].choiceUser[idx].show
 }
 
-const zoomStep = 0.2
-const minScale = 0.2
-const maxScale = 6
-
-const zoomIn = () => {
-  fitMode.value = false; scale.value = Math.min(maxScale, +(scale.value + zoomStep).toFixed(2)) 
-}
-const zoomOut = () => {
-  fitMode.value = false; scale.value = Math.max(minScale, +(scale.value - zoomStep).toFixed(2)) 
-}
-const resetTransform = () => {
-  scale.value = 1; rotate.value = 0; offsetX.value = 0; offsetY.value = 0; fitMode.value = true; nextTickFit() 
-}
-const rotateLeft = () => {
-  rotate.value = (rotate.value - 90) % 360 
-}
-const rotateRight = () => {
-  rotate.value = (rotate.value + 90) % 360 
+const lookDetail = () => {
+  // emit('openChoiceQuestionDetail', props.slideIndex)
 }
 
-const toggleZoom = () => {
-  fitMode.value = false
-  scale.value = scale.value >= 1.8 ? 1 : 2
+const previewImageToolRef = ref<any>(null)
+const previewImage = (url:string) => {
+  previewImageToolRef.value.previewImage(url)
 }
 
-const toggleFit = () => {
-  fitMode.value = !fitMode.value
-  nextTickFit()
-}
 
-const nextTickFit = () => {
-  // 适应屏幕时复位位置与缩放
-  if (fitMode.value) {
-    scale.value = 1
-    offsetX.value = 0
-    offsetY.value = 0
-  }
-}
-
-const onWheel = (e: WheelEvent) => {
-  if (e.deltaY > 0) zoomOut()
-  else zoomIn()
-}
+defineExpose({
+  workDetail,
+  choiceQuestionListData,
+  workIndex,
+  workArray: props.workArray
+})
 
-const onDragStart = (e: MouseEvent) => {
-  dragging.value = true
-  lastX.value = e.clientX
-  lastY.value = e.clientY
-}
-const onDragMove = (e: MouseEvent) => {
-  if (!dragging.value) return
-  const dx = e.clientX - lastX.value
-  const dy = e.clientY - lastY.value
-  lastX.value = e.clientX
-  lastY.value = e.clientY
-  offsetX.value += dx
-  offsetY.value += dy
-}
-const onDragEnd = () => {
-  dragging.value = false 
-}
 </script>
 
 <style lang="scss" scoped>
@@ -583,6 +470,14 @@ const onDragEnd = () => {
 			margin-top: 20px;
 			font-size: 14px;
 			font-weight: 600;
+      img{
+        width: auto;
+        max-width: 100%;
+        height: auto;
+        object-fit: cover;
+        cursor: pointer;
+        display: block;
+      }
 		}
 		.atr_t45a_item {
 			width: 100%;

+ 252 - 0
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -0,0 +1,252 @@
+<template>
+   <div class="choiceQuestionDetailDialog">
+    <div class="content" :style="{
+          width: slideWidth + 'px',
+          height: slideHeight + 'px',
+        }">
+          <span class="closeIcon" @click="closeSlideIndex()">
+            <img src="../../../assets/img/close.png">
+          </span>
+        <div class="c_t45" v-if="workDetail && workDetail.type==='45' && props.showData">
+          <div class="c_t45_title">{{ props.showData.choiceQuestionListData[props.showData.workIndex].teststitle  }}</div>
+          <img class="c_t45_img" :src="props.showData.choiceQuestionListData[props.showData.workIndex].timuList[0].src" v-if="props.showData.choiceQuestionListData[props.showData.workIndex].timuList.length>0">
+          <span class="c_t45_type" v-if="props.showData.choiceQuestionListData[props.showData.workIndex].type==='1'">单选题</span>
+          <span class="c_t45_type" v-if="props.showData.choiceQuestionListData[props.showData.workIndex].type==='2'">多选项</span>
+          <div class="c_t45_echarts" :style="{
+            width: slideWidth-40 + 'px',
+          }">
+            <div id="echartsArea1" ref="echartsArea1"></div>
+          </div>
+        </div>
+      </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch, onUnmounted } from 'vue'
+import * as echarts from 'echarts'
+const props = defineProps<{
+  visible: number[]
+  workIndex: number
+  slideWidth: number
+  slideHeight: number
+  slideIndex: number
+  showData:any
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:visible', v: number[]): void
+}>()
+
+const visible = computed({
+  get: () => props.visible,
+  set: (v: number[]) => emit('update:visible', v)
+})
+
+const workDetail = computed(() => {
+  if (props.showData) {
+    return props.showData.workDetail
+  }
+  return null
+})
+
+// 关闭对应的作业详细页面
+const closeSlideIndex = () => {
+  visible.value = visible.value.filter((v) => v !== props.slideIndex)
+}
+
+// 选择题图表div
+const echartsArea1 = ref<any>(null)
+
+// 选择题图表实例
+const myChart = ref<any>(null)
+
+// 设置选择题图表
+const setEchartsArea1 = () => {
+  if ( !myChart.value && echartsArea1.value) {
+    myChart.value = echarts.init(echartsArea1.value)
+  }
+  else myChart.value = null
+  if (myChart.value) {
+    const _work = props.showData.choiceQuestionListData[props.showData.workIndex]
+    console.log(_work)
+    const option = {
+      tooltip: {},
+      yAxis: {
+        show: false // 不显示最左边的y轴
+      },
+      xAxis: {
+        data: [],
+        axisLabel: {
+          color: 'rgba(0, 0, 0, 0.9)',
+          fontWeight: 600,
+          fontSize: 18,
+          lineHeight: 24
+        }
+      },
+      series: [
+        {
+          name: '',
+          type: 'bar',
+          data: [],
+          barWidth: '50%', // 柱体宽度缩小40%
+          itemStyle: {
+            color: 'rgba(252, 207, 0, 1)'
+          },
+          label: {
+            show: true,
+            position: 'top',
+            color: 'rgba(116, 139, 115, 1)',
+            fontSize: 22,
+            fontWeight: 500,
+            lineHeight: 24
+          }
+        }
+      ]
+    }
+    _work.choiceUser.forEach((i:any) => {
+      option.xAxis.data.push(i.option && typeof i.option === 'object' ? '图片' : i.option)
+      option.series[0].data.push(i.user.length)
+    })
+    console.log('设置echarts')
+    // {
+    //   title: {
+    //     text: 'ECharts 入门示例'
+    //   },
+    //   tooltip: {},
+    //   xAxis: {
+    //     data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
+    //   },
+    //   yAxis: {},
+    //   series: [
+    //     {
+    //       name: '销量',
+    //       type: 'bar',
+    //       data: [5, 20, 36, 10, 10, 20]
+    //     }
+    //   ]
+    // }
+    myChart.value.setOption(option)
+  }
+
+}
+
+// 监听选择题数据变化
+watch(() => props.showData, (newVal, oldVal) => {
+  if (newVal && newVal.choiceQuestionListData[newVal.workIndex] && props.showData.workDetail.type === '45') {
+    if (oldVal && newVal.choiceQuestionListData[newVal.workIndex] && oldVal.choiceQuestionListData[oldVal.workIndex]) {
+      if (JSON.stringify(newVal.choiceQuestionListData[newVal.workIndex]) !== JSON.stringify(oldVal.choiceQuestionListData[oldVal.workIndex])) {
+        setEchartsArea1()
+      }
+    }
+  }
+  else {
+    myChart.value = null
+  }
+}, {immediate: true})
+
+watch(() => props.showData?.workIndex, (newVal, oldVal) => {
+  if (newVal && props.showData.workDetail && props.showData.workDetail.type === '45') {
+    setEchartsArea1()
+  }
+  else {
+    myChart.value = null
+  }
+  
+})
+
+watch(() => echartsArea1.value, (newVal, oldVal) => {
+  if (newVal && props.showData && props.showData.workDetail && props.showData.workDetail.type === '45') {
+    setEchartsArea1()
+  }
+  else {
+    myChart.value = null
+  }
+})
+
+watch(() => props.slideWidth, (newVal) => {
+  if (newVal && props.showData.workDetail && props.showData.workDetail.type === '45') {
+    console.log('宽度变化', newVal)
+    if (myChart.value) {
+      myChart.value.resize()
+    }
+    // setTimeout(() => {
+    //   setEchartsArea1()
+    // }, 1000)
+  }
+})
+
+
+</script>
+
+<style lang="scss" scoped>
+.choiceQuestionDetailDialog{
+  background: none;
+  position: relative;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+  .content{
+    width: 100%;
+    height: 100%;
+    position: relative;
+    background: #fff;
+    box-sizing: border-box;
+    padding: 40px;
+    overflow: auto;
+    .closeIcon{
+      position: absolute;
+      right: 20px;
+      top: 20px;
+      cursor: pointer;
+      width: 20px;
+      height: 20px;
+      img{
+        width: 100%;
+        height: 100%;
+      }
+    }
+    .c_t45{
+      width: 100%;
+      min-height: 100%;
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      height: auto;
+      .c_t45_title{
+        color: rgba(0, 0, 0, 0.9);
+        font-weight: 600;
+        font-size: 20px;
+        line-height: 24px;
+      }
+      .c_t45_img{
+        max-width: 200px;
+        object-fit: cover;
+        margin-top: 20px;
+      }
+      .c_t45_type{
+        font-weight: 400;
+        font-size: 15px;
+        line-height: 21px;
+        letter-spacing: .5px;
+        color: rgba(0, 0, 0, 1);
+        opacity: .5;
+        margin-top: 20px;
+      }
+
+      .c_t45_echarts{
+        width: 100%;
+        flex: 1;
+        min-height: 400px;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        &>div{
+          width: 100%;
+          height: 400px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 25 - 7
src/views/Student/index.vue

@@ -87,7 +87,10 @@
           </div>
 
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
-            :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex"/>
+            :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
+
+          <choiceQuestionDetailDialog v-show="choiceQuestionDetailDialogOpenList.includes(slideIndex)" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+
 
           <div class="slide-bottom" v-if="!isFullscreen">
               <div class="slide-bottom-center" v-if="!isFullscreen && (!isFollowModeActive || props.type == '1')">
@@ -233,8 +236,8 @@
         
         <!-- 回答结果内容 -->
         <div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
-          <!--<div v-if="workLoading" class="homework-loading">正在加载作业...</div>
-          // <answerTheResult :workId="workId" :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" v-else ref="answerTheResultRef"/>-->
+          <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
+          <answerTheResult :workId="workId" :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" />
           <div class="homework-title">已提交</div>
           <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
           <div v-else>
@@ -248,8 +251,8 @@
             </div>
           </div>
           
-          <!-- 未提交作业的学生列表 -->
-          <div v-if="unsubmittedStudents && unsubmittedStudents.length > 0" class="homework-title" style="margin-top: 20px;">未提交</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>
@@ -259,7 +262,7 @@
                 </button>
               </div>
             </div>
-          </div>
+          </div>-->
         </div>
         
         <!-- 对话区内容 -->
@@ -317,7 +320,8 @@ import ChoiceStatistics from './components/ChoiceStatistics.vue'
 import * as Y from 'yjs'
 import { WebsocketProvider } from 'y-websocket'
 import { Refresh } from '@icon-park/vue-next'
-// import answerTheResult from './components/answerTheResult.vue'
+import answerTheResult from './components/answerTheResult.vue'
+import choiceQuestionDetailDialog from './components/choiceQuestionDetailDialog.vue'
 
 
 // 导入图片资源
@@ -367,6 +371,7 @@ const writingBoardToolVisible = ref(false)
 const timerlVisible = ref(false)
 const slideThumbnailModelVisible = ref(false)
 const laserPen = ref(false)
+const answerTheResultRef = ref(null)
 
 // 作业提交状态
 const isSubmitting = ref(false)
@@ -378,6 +383,7 @@ const slideHeight = ref(0)
 const slideWidth2 = ref(0)
 const slideHeight2 = ref(0)
 
+
 // 添加loading状态
 const isLoading = ref(false)
 const workLoading = ref(false)
@@ -398,6 +404,7 @@ const visibleShot = ref(false)
 const visibleQA = ref(false)
 const visibleChoice = ref(false)
 const visibleAI = ref(false)
+const choiceQuestionDetailDialogOpenList = ref<number[]>([])
 
 // 当前作业选择/问答题的ID
 const workId = ref<string>('')
@@ -2045,6 +2052,17 @@ const getMessages = (msgObj: any) => {
 }
 
 
+// 打开作业查看详细
+const openChoiceQuestionDetail = (index:number) => {
+  if (!choiceQuestionDetailDialogOpenList.value.includes(index)) {
+    choiceQuestionDetailDialogOpenList.value.push(index)
+  }
+
+  // else {
+  //   choiceQuestionDetailDialogOpenList.value = choiceQuestionDetailDialogOpenList.value.filter(i => i !== index)
+  // }
+}
+
 onMounted(() => {
   document.addEventListener('keydown', handleKeydown)
 

+ 191 - 0
src/views/components/tool/previewImageTool.vue

@@ -0,0 +1,191 @@
+<template>
+  <!-- 预览放大(带缩放/拖拽/旋转/工具栏) -->
+  <Teleport to="body">
+    <div
+      v-if="previewVisible"
+      class="image-preview"
+      @click.self="closePreview"
+      @wheel.prevent="onWheel"
+    >
+      <div class="image-preview__toolbar">
+        <button @click.stop="zoomOut">-</button>
+        <button @click.stop="zoomIn">+</button>
+        <button @click.stop="resetTransform">重置</button>
+        <button @click.stop="rotateLeft">⟲</button>
+        <button @click.stop="rotateRight">⟳</button>
+        <button @click.stop="toggleFit">
+          {{ fitMode ? "实际大小" : "适应屏幕" }}
+        </button>
+        <button @click.stop="closePreview">关闭</button>
+      </div>
+      <div
+        class="image-preview__stage"
+        @mousedown="onDragStart"
+        @mousemove="onDragMove"
+        @mouseup="onDragEnd"
+        @mouseleave="onDragEnd"
+        @dblclick.stop="toggleZoom"
+      >
+        <!-- 预览中的 Loading 状态 -->
+        <div v-if="previewImageLoading" class="preview-loading">
+          <div class="loading-spinner"></div>
+          <div class="loading-text">图片加载中...</div>
+        </div>
+
+        <img
+          v-show="!previewImageLoading"
+          :src="imageUrl"
+          alt="预览"
+          class="image-preview__img"
+          :style="imgStyle"
+          draggable="false"
+          @load="onPreviewImageLoad"
+          @error="onPreviewImageError"
+        />
+      </div>
+    </div>
+  </Teleport>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, computed, defineExpose } from 'vue'
+const imageUrl = ref<string>('')
+// Loading 状态
+const imageLoading = ref(false)
+const previewImageLoading = ref(false)
+
+const previewImage = (url: string) => {
+  imageUrl.value = url
+  previewVisible.value = true
+}
+
+// 监听图片URL变化,重置loading状态
+watch(imageUrl, (newUrl) => {
+  if (newUrl) {
+    imageLoading.value = true
+    previewImageLoading.value = true
+  }
+})
+
+const previewVisible = ref(false)
+const scale = ref(1)
+const rotate = ref(0)
+const offsetX = ref(0)
+const offsetY = ref(0)
+const dragging = ref(false)
+const lastX = ref(0)
+const lastY = ref(0)
+const fitMode = ref(true)
+
+const imgStyle = computed(() => {
+  const t = `translate(${offsetX.value}px, ${offsetY.value}px) rotate(${rotate.value}deg) scale(${scale.value})`
+  return {
+    transform: t,
+  }
+})
+
+// 图片加载事件处理
+const onImageLoad = () => {
+  imageLoading.value = false
+}
+
+const onImageError = () => {
+  imageLoading.value = false
+  console.error('图片加载失败')
+}
+
+const onPreviewImageLoad = () => {
+  previewImageLoading.value = false
+}
+
+const onPreviewImageError = () => {
+  previewImageLoading.value = false
+  console.error('预览图片加载失败')
+}
+
+const openPreview = () => {
+  if (imageLoading.value) return // 如果主图片还在加载,不允许打开预览
+  previewVisible.value = true
+  previewImageLoading.value = true // 预览时重新显示loading
+  nextTickFit()
+}
+
+const closePreview = () => {
+  previewVisible.value = false
+}
+
+const zoomStep = 0.2
+const minScale = 0.2
+const maxScale = 6
+
+const zoomIn = () => {
+  fitMode.value = false
+  scale.value = Math.min(maxScale, +(scale.value + zoomStep).toFixed(2))
+}
+const zoomOut = () => {
+  fitMode.value = false
+  scale.value = Math.max(minScale, +(scale.value - zoomStep).toFixed(2))
+}
+const resetTransform = () => {
+  scale.value = 1
+  rotate.value = 0
+  offsetX.value = 0
+  offsetY.value = 0
+  fitMode.value = true
+  nextTickFit()
+}
+const rotateLeft = () => {
+  rotate.value = (rotate.value - 90) % 360
+}
+const rotateRight = () => {
+  rotate.value = (rotate.value + 90) % 360
+}
+
+const toggleZoom = () => {
+  fitMode.value = false
+  scale.value = scale.value >= 1.8 ? 1 : 2
+}
+
+const toggleFit = () => {
+  fitMode.value = !fitMode.value
+  nextTickFit()
+}
+
+const nextTickFit = () => {
+  // 适应屏幕时复位位置与缩放
+  if (fitMode.value) {
+    scale.value = 1
+    offsetX.value = 0
+    offsetY.value = 0
+  }
+}
+
+const onWheel = (e: WheelEvent) => {
+  if (e.deltaY > 0) zoomOut()
+  else zoomIn()
+}
+
+const onDragStart = (e: MouseEvent) => {
+  dragging.value = true
+  lastX.value = e.clientX
+  lastY.value = e.clientY
+}
+const onDragMove = (e: MouseEvent) => {
+  if (!dragging.value) return
+  const dx = e.clientX - lastX.value
+  const dy = e.clientY - lastY.value
+  lastX.value = e.clientX
+  lastY.value = e.clientY
+  offsetX.value += dx
+  offsetY.value += dy
+}
+const onDragEnd = () => {
+  dragging.value = false
+}
+
+defineExpose({
+  previewImage,
+})
+</script>
+
+<style lang="scss" scoped></style>