소스 검색

基本完成三个页面的开发 但教师批改页面和学生查看详情页面的返回按钮还需要进一步检讨功能设计 教师端的课程作业API需要和后端对接

Jiang Pengyu 1 년 전
부모
커밋
bfbb8d9b96

+ 550 - 53
src/views/CorrectPage/CorrectPage.vue

@@ -4,36 +4,223 @@
   Created Date: 2024-7-24
 -->
 <template>
+  <div class="correct-page">
+    <!-- 最左侧AI对话区域 -->
+    <div class="ai-chat-panel">
+      <div class="chat-header">
+        <h3 class="chat-title">学生与AI对话记录</h3>
+      </div>
+      
+      <div class="chat-messages">
+        <div 
+          v-for="(message, index) in formattedMessages" 
+          :key="index"
+          class="message-group"
+        >
+          <!-- 用户消息 -->
+          <div v-if="message.role === 'user'" class="user-message">
+            <div class="message-info">
+              <div class="user-name">Student</div>
+              <div class="message-bubble user-bubble">
+              {{ message.content }}
+              </div>
+            </div>
+            <div class="message-avatar user-avatar">
+              <span>S</span>
+            </div>
+          </div>
+          
+          <!-- AI消息 -->
+          <div v-if="message.role === 'assistant'" class="ai-message">
+            <div class="message-avatar ai-avatar">
+              <span>C</span>
+            </div>
+            <div class="message-info">
+              <div class="ai-name">ChatGLM4</div>
+              <div class="message-bubble ai-bubble">
+                {{ message.content }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+       <div class="chat-footer"></div>
+    </div>
+  
+    <!-- 右侧内容区域 -->
+    <div class="right-content">
+      <!-- 作业描述区域 -->
+      <div class="content-panel">
+        <!-- 上半部分:作业描述 -->
+        <div class="assignment-section">
+          <div class="section-header">
+            <h2>英语写作练习</h2>
+          </div>
+          <div class="assignment-content">
+            <StudentWritingRequirements />
+          </div>
+        </div>
+      </div>
 
-  <div class="my-correct">
-    <!-- 写作题目 及 作文内容 -->
-
-    <div class="my-correct-header">
-      <el-card>
-        <StudentWritingRequirements />
-      </el-card>
-      <div style="height: 10px"></div>
-      <el-card>
-        <StudentEssay :studentId="studentId" :assignmentId="assignmentId" />
-      </el-card>
+      <!-- 学生信息和回答区域 -->
+      <div class="student-info-section">
+        <div class="student-header">
+          <span class="student-label">学生姓名:</span>
+          <span class="student-name">{{ studentInfo.name }}</span>
+          <span class="class-label">所属班级:</span>
+          <span class="class-name">{{ studentInfo.className || '信息与计算科学(强基计划)' }}</span>
+        </div>
+        <div class="student-answer-content">
+          {{ studentAnswer }}
+        </div>
+      </div>
     </div>
 
-    <div class="right-side">
-      <!-- update: 仅保留AI历史对话 -->
-      <div class="my-correct-record">
-        <h2>学生与AI对话历史记录</h2>
-        <AIChatter :dialogueId="dataForm.dialogueId" :messages="dataForm.messages" :is-teacher="true" />
-        <el-button @click="showDialog = true">查看行为记录</el-button>
+    <!-- 教师功能区域 -->
+    <div v-if="!isStudent" class="teacher-function-area">
+      <!-- 查看行为记录按钮 -->
+      <el-button 
+        type="primary" 
+        class="behavior-record-btn"
+        @click="handleBehaviorRecord"
+      >
+        查看行为记录
+      </el-button>
+      
+      <!-- 评分区域 -->
+      <div class="rating-section">
+        <div class="rating-component">
+          <div class="rating-input-wrapper">
+            <span style="font-size: 22px; font-weight: 500; margin-left: -20px;">评分:&nbsp;&nbsp;</span>
+            <div class="score-counter">
+              <button 
+                class="score-btn decrease-btn" 
+                @click="decreaseScore"
+                :disabled="correctionData.score <= 1"
+              >
+                −
+              </button>
+              <input 
+                type="number" 
+                class="score-input" 
+                :min="1" 
+                :max="100" 
+                placeholder="暂无分数" 
+                title="请输入1~100间的数字"
+                v-model="correctionData.score" 
+                @input="validateScore" 
+              />
+              <button 
+                class="score-btn increase-btn" 
+                @click="increaseScore"
+                :disabled="correctionData.score >= 100"
+              >
+                +
+              </button>
+            </div>
+          </div>
+          <div v-if="scoreError" class="error-message">{{ scoreError }}</div>
+        </div>
+      </div>
+      
+      <!-- 评语区域 -->
+      <div class="remark-section">
+        <div class="textarea-container">
+          <textarea 
+            v-model="correctionData.remark" 
+            placeholder="请输入评语" 
+            class="remark-textarea"
+          ></textarea>
+        </div>
+        
+        <!-- 提交批改和评语模板按钮 -->
+        
       </div>
 
-      <!-- 教师评分、修改意见及评价 -->
-      <div class="my-correct-remark">
-        <Remark :studentId="studentId" :assignmentId="assignmentId" />
+      <div class="button-section">
+        <!-- 第一行按钮 -->
+        <div class="button-row">
+          <el-button 
+            class="action-btn submit-btn"
+            @click="handleCorrect"
+            :loading="submitting"
+          >
+            提交评语
+          </el-button>
+          <el-button 
+            class="action-btn next-btn"
+            @click="handleNext"
+            :disabled="isAllCorrected"
+          >
+            下一个
+          </el-button>
+        </div>
+        
+        <!-- 第二行按钮 -->
+        <div class="button-row">
+          <el-button 
+            class="action-btn template-btn"
+            @click="showTemplateDialog"
+          >
+            评语模板
+          </el-button>
+          <el-button 
+            class="action-btn back-btn"
+            @click="goBack"
+          >
+            返回
+          </el-button>
+        </div>
+        
+        <!-- 进度条 -->
+        <div class="progress-section">
+          <el-progress 
+            :percentage="progressPercentage" 
+            :stroke-width="20"
+            color="#7FD67F"
+            class="progress-bar"
+          />
+        </div>
+        
+        <!-- 进度文字 -->
+        <div class="progress-text">
+          已批改/未批改:{{ correctionStats.correctNumber }}/{{ uncorrectedNumber }}
+        </div>
       </div>
+      
+      <!-- 预留区域:后续可在此添加更多教师功能 -->
     </div>
-  </div>
-
-  <el-dialog v-model="showDialog" title="文件信息">
+    
+    <!-- 学生功能区域 -->
+    <div v-if="isStudent" class="student-function-area">
+      <!-- 老师批改分数展示 -->
+      <div class="student-score-display">
+        <div class="score-label">评分:</div>
+        <div class="score-container">
+          <div class="score-value">{{ correctionData.score || '暂无评分' }}</div>
+        </div>
+      </div>
+      
+      <!-- 老师评语展示 -->
+      <div class="student-remark-display">
+        <div class="remark-content">
+          {{ correctionData.remark || '老师暂未给出评语' }}
+        </div>
+      </div>
+      
+      <!-- 学生返回按钮区域 -->
+      <div class="student-button-area">
+        <el-button 
+          class="student-back-btn"
+          @click="goBackStudent"
+        >
+          返回
+        </el-button>
+      </div>
+    </div>
+    
+    <!-- 行为记录对话框 -->
+        <el-dialog v-model="showDialog" title="文件信息">
 
       <el-table :data="fileList" stripe>
         <el-table-column prop="id" label="ID"></el-table-column>
@@ -52,31 +239,53 @@
         <el-table-column prop="studentId" label="学生 ID"></el-table-column>
       </el-table>
 
-    <template #footer>
+      <template #footer>
       <el-button @click="showDialog = false">取消</el-button>
       <el-button type="primary" @click="downloadAllFiles">导出</el-button>
-    </template>
-  </el-dialog>
-
+      </template>
+      </el-dialog>
+      
+    <!-- 评语模板对话框 -->
+    <el-dialog
+      v-model="templateDialogVisible"
+      title="评语模板"
+      width="50%"
+      :close-on-click-modal="false"
+      class="template-dialog"
+    >
+      <div class="template-list">
+        <div 
+          v-for="(template, index) in remarkTemplates" 
+          :key="index"
+          class="template-item"
+          @click="selectTemplate(template)"
+        >
+          <div class="template-content">
+            {{ index + 1 }}. {{ template }}
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup lang="ts">
-import AIChatter from "@/components/AIChatter/AIChatter.vue";
-import Dictionaries from "@/views/Home/component/Dictionaries/index.vue";
+
 import useApis from "@/apis";
 import { useStore } from "@/store";
 import router from "@/router"
-import { onMounted, reactive, ref, watchEffect } from "vue";
+import { onMounted, reactive, ref, computed } from "vue";
 import type { engagementVO } from "@/types/vo";
 import type { IDialogue } from "@/types/dialogue.ts";
-import Edit from "@/views/Home/component/Edit/index.vue";
-import Slider from '@/views/Home/component/Silder/index.vue'
+import { ElMessage } from 'element-plus';
+
 import "./index.scss"
 import { useRoute } from 'vue-router'
-import StudentEssay from "../Home/component/StudentEssay/StudentEssay.vue";
-import StudentWritingRequirements from "../Home/component/StudentEssay/StudentWritingRequirements.vue";
-import Remark from "../Home/component/Remark/Remark.vue";
-import dayjs from 'dayjs';
+
+// 导入所需组件
+import StudentWritingRequirements from "@/views/Home/component/StudentEssay/StudentWritingRequirements.vue";
+
+import * as dayjs from 'dayjs';
 const formatDateTime = (isoString: string): string => {
   return dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
 };
@@ -89,10 +298,53 @@ const studentId = Number(route.query.studentId)
 const apis = useApis()
 const store = useStore()
 
+// 弹窗相关变量
 const fileList = reactive([]);
-
 const showDialog = ref(false);
 
+// 评分和评语相关变量
+const correctionData = reactive({
+  score: 0,
+  remark: ''
+});
+const scoreError = ref('');
+const submitting = ref(false);
+
+// 评语模板相关变量
+const templateDialogVisible = ref(false);
+const remarkTemplates = [
+  '希望广大青年坚定理想信念,厚植家国情怀,练就过硬本领,发扬奋斗精神,到祖国和人民最需要的地方发光发热,为中国式现代化建设贡献青春力量',
+  '文章结构清晰,论点明确,语言表达流畅,体现了较好的写作水平。',
+  '内容充实,观点鲜明,但在语法和拼写方面还需要进一步完善。',
+  '思路清晰,表达准确,展现了良好的语言运用能力。',
+  '文章内容丰富,但需要注意段落间的逻辑连接和过渡。'
+];
+
+// 批改进度统计
+const correctionStats = reactive({
+  engageNumber: 0,
+  correctNumber: 0
+});
+
+// 计算批改进度百分比
+const progressPercentage = computed(() => {
+  if (correctionStats.engageNumber === 0) return 0;
+  return Math.round((correctionStats.correctNumber / correctionStats.engageNumber) * 100);
+});
+
+// 计算未批改数量
+const uncorrectedNumber = computed(() => {
+  return correctionStats.engageNumber - correctionStats.correctNumber;
+});
+
+// 判断是否全部批改完成
+const isAllCorrected = computed(() => {
+  return uncorrectedNumber.value === 0;
+});
+
+// 判断当前用户是否为学生
+const isStudent = computed(() => store.user.role === 'STUDENT');
+
 const dataForm = reactive<{
   messages: IDialogue[],
   dialogueId: string;
@@ -103,6 +355,55 @@ const dataForm = reactive<{
   engagement: {} as engagementVO
 })
 
+// 学生信息
+const studentInfo = reactive({
+  name: '',
+  className: ''
+})
+
+// 学生回答内容
+const studentAnswer = ref('')
+
+// 格式化消息数据用于显示
+const formattedMessages = ref<IDialogue[]>([])
+
+// 更新格式化消息
+const updateFormattedMessages = () => {
+  if (!dataForm.messages || dataForm.messages.length === 0) {
+    formattedMessages.value = []
+    return
+  }
+  formattedMessages.value = [...dataForm.messages]
+}
+
+// 获取学生信息
+async function fetchStudentInfo() {
+  try {
+    const response = await apis.findUserById(studentId)
+    if (response.data.code === 200) {
+      const userData = response.data.data.records[0]
+      studentInfo.name = userData.name
+      studentInfo.className = userData.className || ''
+    }
+  } catch (error) {
+    console.error('获取学生信息失败:', error)
+  }
+}
+
+// 获取评分和评语数据
+async function fetchCorrectionData() {
+  try {
+    const response = await apis.getEngagement(studentId, assignmentId)
+    if (response.data.code === 200) {
+      const engagementData = response.data.data
+      correctionData.score = engagementData.score || 0
+      correctionData.remark = engagementData.remark || ''
+    }
+  } catch (error) {
+    console.error('获取评分数据失败:', error)
+  }
+}
+
 // 获取数据
 async function fetchData() {
   await apis.getAIDialogues(assignmentId, studentId)
@@ -113,48 +414,243 @@ async function fetchData() {
         messages: data?.messages.content,
         engagement: dataForm.engagement
       })
+      updateFormattedMessages()
     })
 
   await apis.getEngagement(studentId, assignmentId)
     .then((_res: any) => {
+      const engagementData = _res.data.data
       Object.assign(dataForm, {
         dialogueId: dataForm.dialogueId,
         messages: dataForm.messages,
-        engagement: _res.data.data
+        engagement: engagementData
       })
+      // 获取学生回答内容
+      if (engagementData.textContent) {
+        studentAnswer.value = engagementData.textContent
+      }
     }).catch((_err: any) => {
       console.log(_err)
       // router.push("/login");
       router.go(-1);
     })
+  
+  // 获取学生信息
+  await fetchStudentInfo()
+  
+  // 获取现有的评分和评语
+  await fetchCorrectionData()
+  
+  // 获取批改进度统计
+  await fetchCorrectionStats()
+}
+
+// 弹窗相关函数
+const handleBehaviorRecord = () => {
+  showDialog.value = true
+  // 如果数据为空,重新获取
+  if (fileList.length === 0) {
+    getBehaviorRecords()
+  }
 }
 
+// 获取行为记录数据
 const getBehaviorRecords = () => {
   apis.selectBehaviorRecord(studentId, assignmentId)
-      .then(res => {
-        Object.assign(fileList, res.data.data)
-        // fileList.push(res.data.data)
-        // fileList.value = res.data.data
-      })
-      .catch(err => {
-        console.log(err)
-      })
+    .then(res => {
+      console.log('API返回的原始数据:', res.data)
+      const records = res.data.data || []
+      console.log('解析后的记录数组:', records)
+      console.log('记录数量:', records.length)
+      
+      // 清空并重新填充数据(确保响应式更新)
+      fileList.length = 0
+      if (Array.isArray(records)) {
+        records.forEach(record => {
+          fileList.push(record)
+        })
+      } else {
+        console.warn('返回的数据不是数组:', records)
+      }
+      
+      console.log('fileList 最终内容:', fileList)
+      console.log('fileList 长度:', fileList.length)
+    })
+    .catch(err => {
+      console.error('获取行为记录失败:', err)
+      ElMessage.error('获取行为记录失败')
+      fileList.length = 0
+    })
+}
+
+
+
+// 下载单个文件
+const downloadSingleFile = (fileUrl: string) => {
+  if (!fileUrl) {
+    ElMessage.warning('文件链接不存在')
+    return
+  }
+  const link = document.createElement('a')
+  link.href = fileUrl
+  link.download = ''
+  link.target = '_blank'
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
 }
 
+// 批量下载所有文件(参考old文件的实现)
 const downloadAllFiles = () => {
+  if (fileList.length === 0) {
+    ElMessage.warning('暂无文件可下载')
+    return
+  }
+  
+  let downloadCount = 0
   fileList.forEach((item) => {
-    const link = document.createElement('a');
-    link.href = item.recordFileUrl;
-    link.download = '';
-    link.click();
-    link.remove();
+    if (item.recordFileUrl) {
+      downloadSingleFile(item.recordFileUrl)
+      downloadCount++
+    }
+  })
+  
+  if (downloadCount > 0) {
+    ElMessage.success(`开始下载 ${downloadCount} 个文件`)
+  } else {
+    ElMessage.warning('没有可下载的文件链接')
+  }
+  
+  showDialog.value = false
+}
+
+// 验证评分
+const validateScore = () => {
+  const score = Number(correctionData.score)
+  if (isNaN(score) || score < 1 || score > 100) {
+    scoreError.value = '评分需要在1-100之间'
+  } else {
+    scoreError.value = ''
+  }
+}
+
+// 增加分数
+const increaseScore = () => {
+  if (correctionData.score < 100) {
+    correctionData.score++
+    validateScore()
+  }
+}
+
+// 减少分数
+const decreaseScore = () => {
+  if (correctionData.score > 1) {
+    correctionData.score--
+    validateScore()
+  }
+}
+
+// 提交批改
+const handleCorrect = async () => {
+  if (!correctionData.score) {
+    ElMessage.error("请先评分")
+    return
+  }
+  if (!correctionData.remark) {
+    ElMessage.error("请先输入评语")
+    return
+  }
+  
+  submitting.value = true
+  try {
+    const response = await apis.engagementCorrect(studentId, assignmentId, correctionData)
+    if (response.data.code === 200) {
+      ElMessage.success("批改成功")
+      await fetchCorrectionStats() // 刷新进度统计
+    } else {
+      ElMessage.error("批改异常: " + response.data.msg)
+    }
+  } catch (error) {
+    console.error('批改失败:', error)
+    ElMessage.error("批改失败,请重试")
+  } finally {
+    submitting.value = false
+  }
+}
+
+// 显示评语模板对话框
+const showTemplateDialog = () => {
+  templateDialogVisible.value = true
+}
+
+// 选择评语模板
+const selectTemplate = (template: string) => {
+  correctionData.remark = template
+  templateDialogVisible.value = false
+}
+
+// 获取批改进度统计
+const fetchCorrectionStats = async () => {
+  try {
+    const response = await apis.getAssignmentCorrectionStats(assignmentId);
+    if (response.data.code === 200) {
+      Object.assign(correctionStats, response.data.data);
+    }
+  } catch (error) {
+    console.error('获取批改进度失败:', error);
+  }
+}
+
+// 获取下一个学生作业ID
+const getNextStudentAssignment = async () => {
+  try {
+    const response = await apis.getNextStudentAssignment(assignmentId, studentId);
+    if (response.data.code === 200) {
+      return response.data.data;
+    }
+  } catch (error) {
+    console.error('获取下一个学生失败:', error);
+  }
+  return null;
+}
+
+// 处理下一个学生
+const handleNext = async () => {
+  const nextStudentId = await getNextStudentAssignment();
+  if (nextStudentId) {
+    router.push({
+      name: 'correct',
+      params: { assignmentId: assignmentId },
+      query: { studentId: nextStudentId }
+    }).then(() => {
+      // 强制刷新页面以重新加载数据
+      window.location.reload();
+    });
+    ElMessage.success("已自动跳转到下一个学生");
+  } else {
+    ElMessage.info("没有下一个学生");
+  }
+}
+
+// 返回上一页(教师版)
+const goBack = () => {
+  router.push({
+    name: 'myCorrectWithCourse',
+  }).then(() => {
+    window.location.reload();
+  });
+}
+
+// 返回上一页(学生版)
+const goBackStudent = () => {
+  router.push({ name: 'myHomework' }).then(() => {
+    window.location.reload(); // 强制刷新页面
   });
-  showDialog.value = false;
 };
 
 onMounted(() => {
   fetchData()
-  getBehaviorRecords()
+  getBehaviorRecords() // 页面加载时获取行为记录数据
 })
 
 </script>
@@ -170,3 +666,4 @@ export default {
   name: "CorrectPage"
 }
 </script>
+

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 981 - 3
src/views/CorrectPage/index.scss


+ 22 - 2
src/views/EditPage/EditPage.vue

@@ -25,16 +25,17 @@
   import useApis from "@/apis";
   import {useStore} from "@/store";
   import router from "@/router"
-  import {onMounted, onUnmounted, reactive, ref, watchEffect} from "vue";
+  import {onMounted, onUnmounted, reactive} from "vue";
   import type {engagementVO} from "@/types/vo";
   import type {IDialogue} from "@/types/dialogue.ts";
   import Edit from "@/views/Home/component/Edit/index.vue";
   import Slider from '@/views/Home/component/Silder/index.vue'
   import "./index.scss"
   import {useRoute} from 'vue-router'
-  import {ElMessage, ElMessageBox} from "element-plus";
+  import {ElMessageBox} from "element-plus";
   import IndexedDB from "@/utils/indexedDBUtil";
   import {isModified, registerAllEventListeners, removeAllEventListeners} from "@/views/EditPage/userWritingRecord";
+  import emitter from "@/utils/emitter";
 
   // 获得路由中的assignmentId
   const route = useRoute()
@@ -104,6 +105,23 @@
         })
   }
 
+  // 保存AI对话内容
+  const saveAIDialogue = async () => {
+    if (dataForm.dialogueId && dataForm.messages.length > 0) {
+      try {
+        await apis.requestAI(dataForm.dialogueId, 'ChatGLM4', dataForm.messages)
+        console.log('AI对话内容已保存')
+      } catch (error) {
+        console.error('保存AI对话失败:', error)
+      }
+    }
+  }
+
+  // 监听提交前的保存对话事件
+  emitter.on('save-ai-dialogue-before-submit', async () => {
+    await saveAIDialogue()
+  })
+
   onMounted( () => {
     fetchData()
     // indexedDB.initDB("eventId")
@@ -115,6 +133,8 @@
     // indexedDB.closeDB()
     // indexedDB.deleteDBAll()
     removeAllEventListeners();
+    // 清理事件监听器
+    emitter.off('save-ai-dialogue-before-submit');
   })
 
 </script>

+ 1 - 1
src/views/HomeworkPage/component/HomeworkDetail.vue

@@ -132,7 +132,7 @@ const fetchData = () => {
 
 const handleBack = () => {
    // 跳转到我的作业页面
-   router.push('/home/myHomework')
+   router.push('/home/myHomework').then(() => {window.location.reload()})
 }
 
 onMounted(() => {

+ 3 - 3
src/views/HomeworkPage/index.vue

@@ -197,7 +197,7 @@
   //  查看教师批改结果 跳转至批改页面
   const viewCorrection = (assignmentId:number,studentId:number, type:string, assignmentName:string) => {
     if(type === '写作' || type ==='writing'){
-      router.push(`/home/assignment/${assignmentId}/correct?studentId=${studentId}`)
+      router.push(`/home/assignment/${assignmentId}/correct?studentId=${studentId}`).then(() => {window.location.reload()})
     } else if(type === 'AI口语' || type === 'speaking'){
       router.push({
       path: `/home/assignment/${assignmentId}/airecord`,
@@ -205,14 +205,14 @@
         userId: studentId,
         assignmentId: assignmentId,
         assignmentName: assignmentName
-      }});
+      }}).then(() => {window.location.reload()});
     }
    
 
   }
 
   const handleShowDetail = (assignmentId:number) => {
-    router.push(`/home/homeworkDetail/${assignmentId}`)
+    router.push(`/home/homeworkDetail/${assignmentId}`).then(() => {window.location.reload()})
 
   }
 

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.