Quellcode durchsuchen

Merge branch 'JiangPengYu_20260111_fix_part1' of FanYanPeng/EAIFrontend into host-nju

JiangPengYu vor 7 Monaten
Ursprung
Commit
2bf6a57c35

+ 1 - 1
.env.example

@@ -7,7 +7,7 @@
 # 建议不要删除,只注释。按需取用
 VITE_TITLE = '本地开发'
 # 本地后端服务器端口
-VITE_BASE_URL = 'http://localhost:8082'
+VITE_BASE_URL = 'http://localhost:8080'
 # 线上正式服务器地址
 # VITE_BASE_URL = 'https://air-nju.seec.seecoder.cn/'
 # 线上http服务器地址

+ 3 - 0
.gitignore

@@ -30,3 +30,6 @@ coverage
 *.tsbuildinfo
 
 .env.local
+RESPONSIVE_DESIGN_CHANGES.md
+EDITPAGE_RESPONSIVE_CHANGES.md
+EDITPAGE_LAYOUT_IMPROVEMENTS.md

+ 4 - 2
src/apis/evaluation.ts

@@ -8,7 +8,8 @@ const evaluationApis = {
             params: {
                 assignmentId,
                 studentId
-            }
+            },
+            timeout: 40000 // 设置超时时间为 40 秒
         })
     },
     sentenceEvaluation(assignmentId: number, studentId: number){
@@ -16,7 +17,8 @@ const evaluationApis = {
             params: {
                 assignmentId,
                 studentId
-            }
+            },
+            timeout: 40000 // 设置超时时间为 40 秒
         })
     },
     getOverall(assignmentId: number, studentId: number, version: number){

+ 206 - 15
src/components/AIChatter/AIChatter.module.scss

@@ -27,18 +27,113 @@
     width: 100%;
     height: 80%;
     overflow-y: auto;
+    padding: 10px 0;
 
-    .item {
+    .message-group {
+      margin-bottom: 20px;
+    }
+
+    // 用户消息样式
+    .user-message {
+      display: flex;
+      justify-content: flex-end;
+      align-items: flex-start;
+      gap: 8px;
+
+      .message-info {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+        max-width: 70%;
+      }
+
+      .user-name {
+        font-size: 12px;
+        color: #666;
+        margin-bottom: 4px;
+        font-weight: 500;
+      }
+
+      .message-bubble {
+        padding: 12px 16px;        
+        word-break: break-word;
+        line-height: 1.5;
+      }
+
+      .user-bubble {
+        background:linear-gradient(135deg, #4ca1f4, #5cb3f5, #6ec0f6);
+        color: white;
+        border-radius: 20px;
+      }
+
+      .message-avatar {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-weight: 600;
+        font-size: 14px;
+        flex-shrink: 0;
+      }
+
+      .user-avatar {
+        background: linear-gradient(135deg, #4ca1f4, #5cb3f5, #6ec0f6);
+        color: white;
+      }
+    }
+
+    // AI消息样式
+    .ai-message {
       display: flex;
+      justify-content: flex-start;
       align-items: flex-start;
-      margin-bottom: 15px;
+      gap: 12px;
+      margin-bottom: 16px;
+
+      .message-info {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        max-width: 70%;
+      }
+
+      .ai-name {
+        font-size: 12px;
+        color: #666;
+        margin-bottom: 4px;
+        font-weight: 500;
+      }
 
-      .content {
-        background-color: #f0f0f0;
-        padding: 8px 12px;
-        border-radius: 16px;
+      .message-bubble {
+        padding: 12px 16px;
+        border-radius: 12px;
         word-break: break-word;
-        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+        line-height: 1.5;
+      }
+
+      .ai-bubble {
+        background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+        color: #333;
+        border-radius: 20px;
+      }
+
+      .message-avatar {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-weight: 600;
+        font-size: 14px;
+        flex-shrink: 0;
+      }
+
+      .ai-avatar {
+        background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+        color: #333;
       }
     }
   }
@@ -80,23 +175,119 @@
   background-color: #ffffff;
   border-radius: 10px;
 
-
   .message-box {
     width: 100%;
     height: 34vh;
     overflow: auto;
+    padding: 10px 0;
 
-    .item {
+    .message-group {
+      margin-bottom: 20px;
+    }
+
+    // 用户消息样式(教师视图)
+    .user-message {
       display: flex;
+      justify-content: flex-end;
       align-items: flex-start;
-      margin-bottom: 15px;
+      gap: 12px;
+      margin-bottom: 16px;
+
+      .message-info {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+        max-width: 70%;
+      }
 
-      .content {
-        background-color: #f0f0f0;
-        padding: 8px 12px;
-        border-radius: 16px;
+      .user-name {
+        font-size: 12px;
+        color: #666;
+        margin-bottom: 4px;
+        font-weight: 500;
+      }
+
+      .message-bubble {
+        padding: 12px 16px;
+        border-radius: 12px;
         word-break: break-word;
-        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+        line-height: 1.5;
+      }
+
+      .user-bubble {
+        background: #5cb3f5;
+        color: white;
+        border-bottom-right-radius: 4px;
+      }
+
+      .message-avatar {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-weight: 600;
+        font-size: 14px;
+        flex-shrink: 0;
+      }
+
+      .user-avatar {
+        background: #5cb3f5;
+        color: white;
+      }
+    }
+
+    // AI消息样式(教师视图)
+    .ai-message {
+      display: flex;
+      justify-content: flex-start;
+      align-items: flex-start;
+      gap: 12px;
+      margin-bottom: 16px;
+
+      .message-info {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        max-width: 70%;
+      }
+
+      .ai-name {
+        font-size: 12px;
+        color: #666;
+        margin-bottom: 4px;
+        font-weight: 500;
+      }
+
+      .message-bubble {
+        padding: 12px 16px;
+        border-radius: 12px;
+        word-break: break-word;
+        line-height: 1.5;
+      }
+
+      .ai-bubble {
+        background-color: #f0f2f5;
+        color: #333;
+        border-bottom-left-radius: 4px;
+      }
+
+      .message-avatar {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-weight: 600;
+        font-size: 14px;
+        flex-shrink: 0;
+      }
+
+      .ai-avatar {
+        background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
+        color: #333;
       }
     }
   }

+ 25 - 16
src/components/AIChatter/AIChatter.vue

@@ -24,28 +24,37 @@
       </div>
      </div>
 
-    <!-- 消息区域,采用ChatGPT的交互模式 -->
+    <!-- 消息区域 -->
     <div :class="$style['message-box']" ref="containerTopRef">
       <div
           v-for="(item, index) in messageList"
           :key="index"
-          :class="$style.item"
+          :class="$style['message-group']"
       >
-        <div>
-          <!--展示头像-->
-          <el-avatar
-              :size="32"
-              :shape="'circle'"
-              :src="index % 2 === 0 ? store.user.userAvatar : ''"
-          >
-            {{ index % 2 === 0 ? store.user.name.charAt(0) : data.model.charAt(0) }}
-          </el-avatar>
+        <!-- 用户消息 -->
+        <div v-if="item?.role === 'user'" :class="$style['user-message']">
+          <div :class="$style['message-info']">
+            <div :class="$style['user-name']">{{ props.isTeacher ? 'Student' : 'You' }}</div>
+            <div :class="[$style['message-bubble'], $style['user-bubble']]">
+              {{ item?.content }}
+            </div>
+          </div>
+          <div :class="[$style['message-avatar'], $style['user-avatar']]">
+            <span>{{ props.isTeacher ? 'S' : (store.user.name.charAt(0) || 'Y') }}</span>
+          </div>
         </div>
-        <div style="margin-left: 15px">
-          <!--名字-->
-          <div style="margin-bottom: 2px">{{ index % 2 === 0 ? 'You' : data.model }}</div>
-          <!--内容-->
-          <div :class="$style.content">{{ item?.content }}</div>
+        
+        <!-- AI消息 -->
+        <div v-if="item?.role === 'assistant'" :class="$style['ai-message']">
+          <div :class="[$style['message-avatar'], $style['ai-avatar']]">
+            <span>{{ data.model.charAt(0) }}</span>
+          </div>
+          <div :class="$style['message-info']">
+            <div :class="$style['ai-name']">{{ data.model }}</div>
+            <div :class="[$style['message-bubble'], $style['ai-bubble']]">
+              {{ item?.content }}
+            </div>
+          </div>
         </div>
       </div>
       <div ref="messagesEndRef" />

+ 2 - 2
src/components/ViewsWordEditor/ViewsWordEditor.vue

@@ -28,8 +28,8 @@
 
       <template #footer>
         <div class="button-right">
-          <el-button color="#597d3b" @click="handleClose">取消</el-button>
-          <el-button color="#597d3b" type="primary" @click="handleClose">OK</el-button>
+          <el-button style="background-color: #6A9BD8; color: white;" @click="handleClose">取消</el-button>
+          <el-button style="background-color: #6A9BD8; color: white;" type="primary" @click="handleClose">OK</el-button>
         </div>
       </template>
     </el-dialog>

+ 15 - 15
src/router/index.ts

@@ -16,24 +16,24 @@ const router = createRouter({
     },
 
     // 登录与注册
-    // {
-    //   path: "/",
-    //   redirect: () => {
-    //     window.location.href = "https://p-nju.seec.seecoder.cn/login?from=https://air-nju.seec.seecoder.cn";
-    //     // window.location.href = "http://localhost:8000/login?from=http://localhost:4000";
-    //     // {本地门户首页地址}?from={本地eai首页地址}
-    //     return "/"; // 占位返回值,实际不会执行
-    // },
-    // },
     {
       path: "/",
-      redirect: "/login"
+      redirect: () => {
+        window.location.href = "https://p-nju.seec.seecoder.cn/login?from=https://air-nju.seec.seecoder.cn";
+        // window.location.href = "http://localhost:8000/login?from=http://localhost:4000";
+        // {本地门户首页地址}?from={本地eai首页地址}
+        return "/"; // 占位返回值,实际不会执行
     },
-    {
-      path: '/login',
-      name: 'login',
-      component: () => import('../views/LoginPage/LoginPage.vue')
     },
+    // {
+    //   path: "/",
+    //   redirect: "/login"
+    // },
+    // {
+    //   path: '/login',
+    //   name: 'login',
+    //   component: () => import('../views/LoginPage/LoginPage.vue')
+    // },
     {
       path: '/register',
       name: 'register',
@@ -260,7 +260,7 @@ const router = createRouter({
  */
   router.beforeEach(async (to) => {
     const store = useStore();
-    const publicPaths = [ '/register', '/passwordChange', '/admin/login','/login'];
+    const publicPaths = [ '/register', '/passwordChange', '/admin/login'];
     // 1. 检查URL中的token
     const urlToken = new URLSearchParams(window.location.search).get('token');
     if (urlToken) {

+ 608 - 96
src/views/AIEvaluationPage/AIEvaluationPage.vue

@@ -1,21 +1,21 @@
 <template>
-  <div style="display: flex">
+  <div class="ai-evaluation-container">
     <!-- 1.作文展示区域 -->
-    <div style="width: 40%; height: 90vh; margin: 10px; position: relative;">
-      <el-breadcrumb :separator-icon="ArrowRight" style="height: 30px; align-content: center; margin-left: 6px">
+    <div class="essay-section">
+      <el-breadcrumb :separator-icon="ArrowRight" class="breadcrumb">
         <el-breadcrumb-item :to="{ path: `/home/Assignment/${assignmentId}/edit` }">写作</el-breadcrumb-item>
         <el-breadcrumb-item><span style="color: rgba(22,119,255,0.62); font-weight: bold; cursor:pointer;">智能批改</span></el-breadcrumb-item>
       </el-breadcrumb>
 
-      <el-card  shadow="never">
+      <el-card shadow="never" class="essay-card">
         <!-- 原文标识 -->
-        <div style="display: flex;justify-content: center;align-items: center;width: 100px; height: 50px; background-color:var(--system-color); position: absolute; top: 40px; left: 0; clip-path: polygon(0 0, 100% 0, 80% 50%, 100% 100%, 0 100%);">
-          <span style="margin-left: -20px;color: white">原文</span>
+        <div class="original-text-label">
+          <span>原文</span>
         </div>
         <!-- 作文内容展示区域 -->
-        <div style="white-space: pre-wrap; margin-top: 80px; line-height: 30px">
-          <el-scrollbar height="760px">
-          <span style="position: relative;">
+        <div class="essay-content-wrapper">
+          <el-scrollbar class="essay-scrollbar">
+          <span class="essay-content">
             <template v-if="data.sentenceEvaluationList?.length != 0">
               <span v-for="sentenceEvaluation in data.sentenceEvaluationList"
                     :key="sentenceEvaluation.id"
@@ -43,9 +43,9 @@
 
 
     <!-- 2.AI评价区域 -->
-    <div style="width: 40%; margin: 10px; height: 89vh;">
+    <div class="evaluation-section">
       <div class="combined-box">
-        <div style="display: flex">
+        <div class="tab-container">
           <div
               class="trapezoid"
               :class="{ active: selected === 'trapezoid' }"
@@ -63,21 +63,23 @@
         </div>
         <div class="rectangle">
           <template v-if="selected === 'trapezoid'">
-            <el-scrollbar height="820px">
-              <div style="display: flex;align-items: flex-start;margin-bottom: 15px;">
-                <div>
-                  <!--展示头像-->
-                  <!--<el-avatar src="@/image/seecoderLogo.png" :size="32" :shape="'circle'">-->
-                  <!--</el-avatar>-->
-                  <div style="border-radius: 50%; background-color:#fff; width: 40px; height: 40px;display: flex;justify-content: center;align-items: center;">
+            <el-scrollbar height="100%" class="evaluation-scrollbar">
+              <!-- 整体评价加载状态 -->
+              <div v-if="isLoadingOverall" class="loading-container">
+                <el-icon class="is-loading" :size="40" color="#409EFF">
+                  <Loading />
+                </el-icon>
+                <div class="loading-text">AI 正在生成整体评价,请稍候...</div>
+              </div>
+              <div v-else class="overall-evaluation-content">
+                <div class="avatar-container">
+                  <div class="avatar-wrapper">
                     <img src="@/image/seecoderLogo.png" width="32" >
                   </div>
                 </div>
-                <div style="margin-left: 15px">
-                  <!--名字-->
-                  <div style="margin-bottom: 2px">SEEAI英语助手</div>
-                  <!--内容-->
-                  <div style="background-color: #f0f0f0;padding: 8px 12px;border-radius: 16px;white-space: pre-line;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);">
+                <div class="evaluation-message">
+                  <div class="assistant-name">SEEAI英语助手</div>
+                  <div class="message-content">
                     {{ data.overallEvaluation?.evaluation }}
                   </div>
                 </div>
@@ -85,22 +87,32 @@
             </el-scrollbar>
           </template>
           <template v-else>
-              <el-scrollbar height="820px">
-                <div style="display: flex; flex-direction: column;">
+              <el-scrollbar height="100%" class="evaluation-scrollbar">
+                <!-- 逐句评价加载状态 -->
+                <div v-if="isLoadingSentence" class="loading-container">
+                  <el-icon class="is-loading" :size="40" color="#409EFF">
+                    <Loading />
+                  </el-icon>
+                  <div class="loading-text">AI 正在生成逐句评价,请稍候...</div>
+                  <div class="loading-tip">由于需要对每个句子进行详细分析,此过程可能需要较长时间</div>
+                </div>
+                <div v-else class="sentence-evaluation-content">
                   <template v-if="data.sentenceEvaluationList?.length == 0">
-                    暂无逐句评价
+                    <div class="no-evaluation">暂无逐句评价</div>
                   </template>
                   <el-card v-for="sentenceEvaluation in data.sentenceEvaluationList"
+                           v-else
                            :key="sentenceEvaluation.id"
-                           style="cursor: pointer; margin-bottom: 10px; position: relative;" shadow="hover"
+                           class="sentence-card" 
+                           shadow="hover"
                            @mouseenter="highlightSentenceId = sentenceEvaluation.id"
                            @mouseleave="highlightSentenceId = -1"
                   >
-                    <div :style="{display: 'flex',justifyContent: 'center',alignItems: 'center',width: '20px', height: '100%', backgroundColor: getTextDecorationColor(sentenceEvaluation), position: 'absolute', top: '0', left: 'calc(100% - 20px)'}">
+                    <div class="color-indicator" :style="{backgroundColor: getTextDecorationColor(sentenceEvaluation)}">
                     </div>
-                    <div style="padding-right: 30px">
-                      <div style="font-weight: bold; line-height: 30px">{{ sentenceEvaluation.category }}</div>
-                      {{ sentenceEvaluation.evaluation }}
+                    <div class="sentence-content">
+                      <div class="sentence-category">{{ sentenceEvaluation.category }}</div>
+                      <div class="sentence-evaluation">{{ sentenceEvaluation.evaluation }}</div>
                     </div>
                   </el-card>
                 </div>
@@ -111,44 +123,15 @@
     </div>
 
     <!-- 3.历史版本选择区域 -->
-    <el-card
-        style="
-          width: 15%;  /* 固定宽度代替百分比 */
-          height: 90vh;
-          margin: 10px 0 10px auto;  /* 关键:左侧自动边距 */
-          border-left: 1px solid #e4e7ed;
-          border-radius: 0 8px 8px 0;
-          box-shadow: -2px 0 4px rgba(0,0,0,0.05);
-          /*position: sticky;  !* 新增固定定位 *!*/
-          right: 0;
-      "
-        shadow="never"
-    >
-      <div style="
-        position: relative;
-        height: 100%;
-        padding: 20px 10px;
-      ">
+    <el-card class="version-section" shadow="never">
+      <div class="version-container">
         <!-- 版本选择标题 -->
-        <div style="
-          position: absolute;
-          top: 0;
-          left: -20px;
-          width: 80px;
-          height: 40px;
-          background: var(--system-color);
-          clip-path: polygon(0 0, 100% 0, 90% 50%, 100% 100%, 0 100%);
-          color: white;
-          display: flex;
-          align-items: center;
-          padding-left: 12px;
-          font-size: 13px;
-        ">
+        <div class="version-header">
           版本历史
         </div>
 
         <!-- 版本列表 -->
-        <el-scrollbar height="800px" style="margin-top: 50px;">
+        <el-scrollbar height="800px" class="version-scrollbar">
           <div
               v-for="version in data.versionList"
               :key="version.id"
@@ -165,14 +148,7 @@
         </el-scrollbar>
 
         <!-- 当前版本提示 -->
-        <div style="
-          position: absolute;
-          bottom: 10px;
-          left: 10px;
-          right: 10px;
-          font-size: 12px;
-          color: #666;
-        ">
+        <div class="current-version">
           当前选择:v{{ selectedVersion }}
         </div>
       </div>
@@ -181,13 +157,13 @@
 </template>
 
 <script setup lang="ts">
-  import {onMounted, reactive, ref, watch} from "vue";
+  import {onMounted, onUnmounted, reactive, ref, watch} from "vue";
   import useApis from "@/apis";
   import MyComponent from "@/views/AIEvaluationPage/MyComponent.vue";
   import {ElMessage} from "element-plus";
   import {useRoute} from "vue-router";
   import {useStore} from "@/store";
-  import { ArrowRight } from "@element-plus/icons-vue";
+  import { ArrowRight, Loading } from "@element-plus/icons-vue";
 
   interface recordVersion {
     id: number,
@@ -211,6 +187,10 @@
     versionList: []
   })
   const selectedVersion = ref(1.2)
+  const isLoadingSentence = ref(false) // 逐句评价加载状态
+  const isLoadingOverall = ref(false) // 整体评价加载状态
+  let sentenceLoadingTimer: NodeJS.Timeout | null = null // 逐句评价延迟加载计时器
+  let sentencePollingInterval: NodeJS.Timeout | null = null // 逐句评价轮询计时器
 
   const toEvaluation = () => {
     apis.overAllEvaluation(assignmentId, store.user.id)
@@ -237,29 +217,111 @@
         })
   }
 
+  // 轮询获取逐句评价(在60秒内每5秒轮询一次)
+  const pollSentenceEvaluation = (version: number) => {
+    let pollCount = 0
+    const maxPolls = 12 // 60秒 / 5秒 = 12次
+    
+    sentencePollingInterval = setInterval(() => {
+      pollCount++
+      
+      // 如果已经轮询了12次(60秒),停止轮询
+      if (pollCount >= maxPolls) {
+        if (sentencePollingInterval) {
+          clearInterval(sentencePollingInterval)
+          sentencePollingInterval = null
+        }
+        isLoadingSentence.value = false
+        return
+      }
+      
+      // 请求逐句评价
+      apis.getSentence(assignmentId, store.user.id, version)
+        .then(res => {
+          if(res.data.code === 200 && res.data.data && res.data.data.length > 0) {
+            // 获取到数据了,停止轮询
+            data.sentenceEvaluationList = res.data.data
+            isLoadingSentence.value = false
+            
+            if (sentencePollingInterval) {
+              clearInterval(sentencePollingInterval)
+              sentencePollingInterval = null
+            }
+            
+            // 检查数据完整性
+            const hasEvaluation = res.data.data.some(item => item.evaluation)
+            if(!hasEvaluation) {
+              console.warn('⚠️ 后端返回的逐句评价数据缺少 evaluation 字段')
+            }
+          }
+        })
+        .catch(err => {
+          console.error('轮询逐句评价失败:', err)
+        })
+    }, 5000) // 每5秒轮询一次
+  }
+
   const getEvaluation = (version: number) => {
     selectedVersion.value = version
+    
+    // 清除之前的计时器和轮询
+    if (sentenceLoadingTimer) {
+      clearTimeout(sentenceLoadingTimer)
+      sentenceLoadingTimer = null
+    }
+    if (sentencePollingInterval) {
+      clearInterval(sentencePollingInterval)
+      sentencePollingInterval = null
+    }
+    
+    // 开始加载整体评价
+    isLoadingOverall.value = true
     apis.getOverall(assignmentId,store.user.id,version)
         .then(res => {
-          // console.log(res)
           data.overallEvaluation = res.data.data
         })
         .catch(err => {
           console.log(err)
           data.overallEvaluation = {} as OverallEvaluation
         })
+        .finally(() => {
+          isLoadingOverall.value = false
+        })
+    
+    // 开始加载逐句评价
+    isLoadingSentence.value = true
     apis.getSentence(assignmentId,store.user.id,version)
         .then(res => {
-          // console.log(res)
           if(res.data.code === 200){
-            data.sentenceEvaluationList = res.data.data
+            const responseData = res.data.data
+            
+            // 如果返回了数据,直接使用
+            if(responseData && responseData.length > 0) {
+              data.sentenceEvaluationList = responseData
+              isLoadingSentence.value = false
+              
+              // 检查数据完整性
+              const hasEvaluation = responseData.some(item => item.evaluation)
+              if(!hasEvaluation) {
+                console.warn('⚠️ 后端返回的逐句评价数据缺少 evaluation 字段,请检查后端 AI 评价生成逻辑')
+              }
+            } else {
+              // 如果返回空数据,开始轮询(每5秒一次,持续60秒)
+              data.sentenceEvaluationList = []
+              pollSentenceEvaluation(version)
+            }
           } else {
+            console.warn('后端返回非200状态码:', res.data.code, res.data.msg)
             data.sentenceEvaluationList = []
+            // 非200状态码也开始轮询
+            pollSentenceEvaluation(version)
           }
         })
         .catch(err => {
-          console.log(err)
+          console.error('逐句评价API请求失败:', err)
           data.sentenceEvaluationList = []
+          // 请求失败也尝试轮询
+          pollSentenceEvaluation(version)
         })
   }
 
@@ -316,6 +378,18 @@
     // getEvaluation()
     getEvaluationVersionList()
   })
+
+  // 组件卸载时清理定时器
+  onUnmounted(() => {
+    if (sentenceLoadingTimer) {
+      clearTimeout(sentenceLoadingTimer)
+      sentenceLoadingTimer = null
+    }
+    if (sentencePollingInterval) {
+      clearInterval(sentencePollingInterval)
+      sentencePollingInterval = null
+    }
+  })
 </script>
 
 <script lang="ts">
@@ -325,34 +399,145 @@ export default {
 </script>
 
 <style scoped>
+/* 主容器 - 响应式布局 */
+.ai-evaluation-container {
+  display: flex;
+  align-items: stretch; /* 让所有子元素高度一致 */
+  gap: 10px;
+  padding: 10px;
+  height: calc(100vh - 80px); /* 使用视口高度减去顶部导航栏等空间 */
+  box-sizing: border-box;
+}
+
+/* 作文展示区域 */
+.essay-section {
+  flex: 0 0 40%;
+  min-width: 300px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+}
+
+.breadcrumb {
+  height: 30px;
+  align-content: center;
+  margin-left: 6px;
+  margin-bottom: 10px;
+}
+
+.essay-card {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.essay-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.original-text-label {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100px;
+  height: 50px;
+  background-color: #87CEEB;
+  position: absolute;
+  top: 0;
+  left: 0;
+  clip-path: polygon(0 0, 100% 0, 80% 50%, 100% 100%, 0 100%);
+  z-index: 10;
+}
+
+.original-text-label span {
+  margin-left: -20px;
+  color: white;
+  font-weight: 500;
+}
+
+.essay-content-wrapper {
+  white-space: pre-wrap;
+  margin-top: 40px;
+  line-height: 30px;
+  flex: 1; /* 自动填充剩余空间 */
+  overflow: hidden;
+  min-height: 0; /* 关键:允许flex子元素收缩并显示滚动条 */
+}
+
+.essay-scrollbar {
+  height: 100%;
+  padding-right: 10px; /* 滚动条右移 */
+}
+
+/* 确保 el-scrollbar 内部元素正确显示 */
+.essay-scrollbar :deep(.el-scrollbar__wrap) {
+  max-height: 100%;
+}
+
+.essay-scrollbar :deep(.el-scrollbar__view) {
+  display: block;
+}
+
+.essay-content {
+  position: relative;
+  display: block;
+  padding-right: 15px; /* 确保内容不被滚动条遮挡 */
+  padding-bottom: 20px; /* 确保最后一行不被截断 */
+  white-space: pre-wrap; /* 保留换行符并自动换行 */
+  word-wrap: break-word; /* 长单词换行 */
+  overflow-wrap: break-word; /* 确保内容不会溢出 */
+}
+
+/* AI评价区域 */
+.evaluation-section {
+  flex: 0 0 40%;
+  min-width: 300px;
+  overflow: hidden;
+}
 
 .combined-box {
-  width: 100%; /* 整体宽度 */
+  width: 100%;
   height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.tab-container {
+  display: flex;
+  flex-shrink: 0;
 }
 
 .trapezoid {
-  width: 100px; /* 梯形底边长度 */
+  width: 100px;
   height: 0;
-  border-bottom: 30px solid #cfcfcf; /* 梯形高度和颜色 */
+  border-bottom: 30px solid #cfcfcf;
   border-right: 20px solid transparent;
-  line-height: 30px; /* 与 border-bottom 高度相等 */
-  text-align: center; /* 水平居中 */
+  line-height: 30px;
+  text-align: center;
   z-index: 1;
   cursor: pointer;
+  transition: all 0.3s;
+  font-size: 14px;
 }
 
 .trapezoid2 {
-  width: 100px; /* 梯形底边长度 */
+  width: 100px;
   height: 0;
-  border-bottom: 30px solid #cfcfcf; /* 梯形高度和颜色 */
+  border-bottom: 30px solid #cfcfcf;
   border-right: 20px solid transparent;
   border-left: 20px solid transparent;
   margin-left: -20px;
   z-index: 2;
-  line-height: 30px; /* 与 border-bottom 高度相等 */
-  text-align: center; /* 水平居中 */
+  line-height: 30px;
+  text-align: center;
   cursor: pointer;
+  transition: all 0.3s;
+  font-size: 14px;
 }
 
 .trapezoid.active {
@@ -366,14 +551,177 @@ export default {
 }
 
 .rectangle {
-  width: 100%; /* 长方形宽度 */
-  height: calc(100% - 30px - 2vh); /* 长方形高度 */
-  border-top: none; /* 去掉顶部边框,使其与梯形无缝连接 */
-  /* 长方形底部的圆角 */
+  width: 100%;
+  flex: 1;
+  border-top: none;
+  background-color: #fff;
+  border-radius: 0 5px 5px 5px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  min-height: 0; /* 重要:允许flexbox子元素正确收缩 */
+}
+
+/* 评价内容滚动区域 */
+.evaluation-scrollbar {
+  flex: 1;
+  min-height: 0;
+}
+
+.overall-evaluation-content {
+  display: flex;
+  align-items: flex-start;
+  padding: 20px;
+  min-height: 100%;
+}
+
+.avatar-container {
+  flex-shrink: 0;
+}
+
+.avatar-wrapper {
+  border-radius: 50%;
+  background-color: #fff;
+  width: 40px;
+  height: 40px;
+  display: flex;
+  justify-content: center;
   align-items: center;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.evaluation-message {
+  margin-left: 15px;
+  flex: 1;
+  min-width: 0; /* 允许文本换行 */
+}
+
+.assistant-name {
+  margin-bottom: 8px;
+  font-weight: 500;
+  color: #333;
+}
+
+.message-content {
+  background-color: #f0f0f0;
+  padding: 12px 16px;
+  border-radius: 16px;
+  white-space: pre-line;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+  word-wrap: break-word;
+  overflow-wrap: break-word;
+  line-height: 1.6;
+}
+
+.sentence-evaluation-content {
+  display: flex;
+  flex-direction: column;
   padding: 20px;
-  background-color: #fff; /* 长方形的背景颜色 */
-  border-radius: 0 5px 5px 5px;
+  gap: 10px;
+}
+
+.no-evaluation {
+  text-align: center;
+  color: #999;
+  padding: 40px 20px;
+  font-size: 14px;
+}
+
+/* 加载状态样式 */
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 300px;
+  padding: 40px 20px;
+}
+
+.loading-text {
+  margin-top: 20px;
+  font-size: 16px;
+  color: #409EFF;
+  font-weight: 500;
+}
+
+.loading-tip {
+  margin-top: 10px;
+  font-size: 12px;
+  color: #999;
+  text-align: center;
+  max-width: 80%;
+  line-height: 1.5;
+}
+
+.sentence-card {
+  cursor: pointer;
+  position: relative;
+  transition: all 0.2s;
+}
+
+.color-indicator {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 20px;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: calc(100% - 20px);
+  border-radius: 0 4px 4px 0;
+}
+
+.sentence-content {
+  padding-right: 30px;
+}
+
+.sentence-category {
+  font-weight: bold;
+  line-height: 30px;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.sentence-evaluation {
+  line-height: 1.6;
+  color: #666;
+  word-wrap: break-word;
+  overflow-wrap: break-word;
+}
+
+/* 版本历史区域 */
+.version-section {
+  flex: 0 0 20%;
+  min-width: 180px;
+  border-left: 1px solid #e4e7ed;
+  border-radius: 0 8px 8px 0;
+  box-shadow: -2px 0 4px rgba(0,0,0,0.05);
+}
+
+.version-container {
+  position: relative;
+  height: 100%;
+  padding: 20px 10px;
+}
+
+.version-header {
+  position: absolute;
+  top: 0;
+  left: -20px;
+  width: 80px;
+  height: 40px;
+  background: #87CEEB;
+  clip-path: polygon(0 0, 100% 0, 90% 50%, 100% 100%, 0 100%);
+  color: white;
+  display: flex;
+  align-items: center;
+  padding-left: 12px;
+  font-size: 13px;
+  font-weight: 500;
+}
+
+.version-scrollbar {
+  margin-top: 50px;
 }
 
 .version-item {
@@ -392,8 +740,8 @@ export default {
 }
 
 .version-item.active {
-  background: rgba(89, 125, 59, 0.1);
-  border: 1px solid rgba(89, 125, 59, 0.3);
+  background: #E0F7FF;
+  border: 1px solid rgba(135, 206, 235, 0.8);
   box-sizing: border-box;
 }
 
@@ -403,19 +751,183 @@ export default {
   border-radius: 50%;
   background: #cfcfcf;
   margin-right: 10px;
+  flex-shrink: 0;
 }
 
 .version-item.active .version-marker {
-  background: var(--system-color);
+  background: #87CEEB;
+}
+
+.version-info {
+  flex: 1;
+  min-width: 0;
 }
 
 .version-time {
   font-size: 12px;
   color: #333;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 .version-author {
   font-size: 10px;
   color: #999;
+  margin-top: 2px;
+}
+
+.current-version {
+  position: absolute;
+  bottom: 10px;
+  left: 10px;
+  right: 10px;
+  font-size: 12px;
+  color: #666;
+  text-align: center;
+  padding: 8px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
+
+/* 响应式设计 */
+/* 平板设备 */
+@media screen and (max-width: 1200px) {
+  .ai-evaluation-container {
+    flex-wrap: wrap;
+    height: auto; /* 允许换行时自动调整高度 */
+    min-height: calc(100vh - 80px);
+  }
+  
+  .essay-section,
+  .evaluation-section {
+    flex: 0 0 calc(50% - 10px);
+    min-width: 280px;
+    height: calc(50vh - 50px); /* 平板模式下各占一半视口 */
+  }
+  
+  .essay-content-wrapper {
+    height: calc(100% - 80px);
+  }
+  
+  .version-section {
+    flex: 0 0 100%;
+    height: 40vh;
+    min-height: 300px;
+    border-left: none;
+    border-top: 1px solid #e4e7ed;
+    border-radius: 8px;
+  }
+  
+  .version-header {
+    left: 0;
+    top: -10px;
+  }
+}
+
+/* 移动设备 */
+@media screen and (max-width: 768px) {
+  .ai-evaluation-container {
+    flex-direction: column;
+    padding: 5px;
+    gap: 8px;
+    height: auto; /* 移动端垂直排列,高度自适应 */
+  }
+  
+  .essay-section,
+  .evaluation-section,
+  .version-section {
+    flex: 0 0 auto;
+    width: 100%;
+    min-width: auto;
+  }
+  
+  .essay-section {
+    height: 45vh;
+  }
+  
+  .essay-content-wrapper {
+    height: calc(100% - 80px);
+  }
+  
+  .evaluation-section {
+    height: 45vh;
+  }
+  
+  .version-section {
+    height: 35vh;
+  }
+  
+  .trapezoid,
+  .trapezoid2 {
+    width: 80px;
+    font-size: 12px;
+  }
+  
+  .overall-evaluation-content {
+    padding: 15px;
+  }
+  
+  .avatar-wrapper {
+    width: 32px;
+    height: 32px;
+  }
+  
+  .avatar-wrapper img {
+    width: 24px !important;
+  }
+  
+  .evaluation-message {
+    margin-left: 10px;
+  }
+  
+  .message-content {
+    padding: 10px 12px;
+    font-size: 14px;
+  }
+  
+  .sentence-evaluation-content {
+    padding: 15px;
+  }
+}
+
+/* 小屏幕移动设备 */
+@media screen and (max-width: 480px) {
+  .breadcrumb {
+    font-size: 12px;
+  }
+  
+  .original-text-label {
+    width: 80px;
+    height: 40px;
+    font-size: 12px;
+  }
+  
+  .essay-content {
+    font-size: 14px;
+    line-height: 24px;
+  }
+  
+  .trapezoid,
+  .trapezoid2 {
+    width: 70px;
+    font-size: 11px;
+    border-bottom-width: 25px;
+    line-height: 25px;
+  }
+  
+  .sentence-category {
+    font-size: 13px;
+  }
+  
+  .sentence-evaluation {
+    font-size: 12px;
+  }
+  
+  .version-header {
+    width: 70px;
+    height: 35px;
+    font-size: 11px;
+  }
 }
 </style>

+ 73 - 7
src/views/BehaviorPage/BehaviorBoard.vue

@@ -4,8 +4,11 @@ import {Back} from "@element-plus/icons-vue";
 import {computed, onMounted, provide, type Ref, ref, watch} from "vue";
 import {ElLoading, ElMessage} from "element-plus";
 import useApis from "@/apis";
+import {useRoute} from "vue-router";
 
 const apis = useApis()
+const route = useRoute()
+
 //pageHeader
 interface CourseVO{
   courseName: string;
@@ -55,6 +58,11 @@ function getCourseList(){
     target: pageHTML,
     background: '#F5F5F59E'
   })
+  
+  // 从路由参数中获取课程ID和作业ID
+  const courseIdFromRoute = route.query.courseId ? Number(route.query.courseId) : null
+  const assignmentIdFromRoute = route.query.assignmentId ? Number(route.query.assignmentId) : null
+  
   apis.getCourseByTeacherId(0,100)
       .then((res: any)=>{
         if (res&&res.data.code===200){
@@ -64,14 +72,28 @@ function getCourseList(){
             return {courseName: item.courseName, courseId: item.courseId}
           })
           if (courseList.value&&courseList.value.length>0){
-            selectedCourse.value = courseList.value[0];
+            // 如果路由中有课程ID参数,尝试选中该课程
+            if (courseIdFromRoute) {
+              const targetCourse = courseList.value.find(c => c.courseId === courseIdFromRoute)
+              if (targetCourse) {
+                selectedCourse.value = targetCourse
+                console.log("从路由参数选中课程:", targetCourse.courseName)
+              } else {
+                selectedCourse.value = courseList.value[0]
+              }
+            } else {
+              selectedCourse.value = courseList.value[0]
+            }
           }
           else {
             selectedCourse.value = {courseName: "", courseId: -1}
             loading.close();
             return;
           }
-          apis.getAssignmentByCourseId(courseData[0].courseId,0,1000)
+          
+          // 获取选中课程的作业列表
+          const targetCourseId = selectedCourse.value.courseId
+          apis.getAssignmentByCourseId(targetCourseId,0,1000)
             .then((res: any)=>{
               if (res&&res.data.code===200){
                 loading.close();
@@ -86,7 +108,18 @@ function getCourseList(){
                 console.log(assignmentList.value);
                 // assignmentData = temp as any[];
                 if (assignmentList.value&&assignmentList.value.length>0){
-                  selectedAssignment.value = assignmentList.value[0];
+                  // 如果路由中有作业ID参数,尝试选中该作业
+                  if (assignmentIdFromRoute) {
+                    const targetAssignment = assignmentList.value.find(a => a.assignmentId === assignmentIdFromRoute)
+                    if (targetAssignment) {
+                      selectedAssignment.value = targetAssignment
+                      console.log("从路由参数选中作业:", targetAssignment.assignmentName)
+                    } else {
+                      selectedAssignment.value = assignmentList.value[0]
+                    }
+                  } else {
+                    selectedAssignment.value = assignmentList.value[0]
+                  }
                 }
                 else{
                   selectedAssignment.value = {assignmentName: "", assignmentId: -1}
@@ -119,6 +152,10 @@ function getAssignmentList(courseId:number){
     target: pageHTML,
     background: '#F5F5F59E'
   })
+  
+  // 从路由参数中获取作业ID(仅在用户手动切换课程时不使用)
+  const assignmentIdFromRoute = route.query.assignmentId ? Number(route.query.assignmentId) : null
+  
   apis.getAssignmentByCourseId(courseId,0,1000)
       .then((res: any)=>{
         if (res&&res.data.code===200){
@@ -130,7 +167,18 @@ function getAssignmentList(courseId:number){
           })
           // assignmentData = temp as any[];
           if (assignmentList.value&&assignmentList.value.length>0){
-            selectedAssignment.value = assignmentList.value[0];
+            // 如果路由中有作业ID参数且该作业在列表中,选中该作业
+            if (assignmentIdFromRoute) {
+              const targetAssignment = assignmentList.value.find(a => a.assignmentId === assignmentIdFromRoute)
+              if (targetAssignment) {
+                selectedAssignment.value = targetAssignment
+                console.log("选中路由参数中的作业:", targetAssignment.assignmentName)
+              } else {
+                selectedAssignment.value = assignmentList.value[0]
+              }
+            } else {
+              selectedAssignment.value = assignmentList.value[0]
+            }
           }
           else{
             selectedAssignment.value = {assignmentName: "", assignmentId: -1}
@@ -205,16 +253,34 @@ function AssignmentChanged(value:AssignmentVO){
     currentEngagementIndex.value = -1;
     return;
   }
+  
+  // 从路由参数中获取学生ID
+  const studentIdFromRoute = route.query.studentId ? Number(route.query.studentId) : null
+  
   apis.getEngagementByAssignmentId(value.assignmentId,0,10000)
       .then((res:any) => {
         console.log(res);
         if (res.data.code === 200){
           engagements.value = res.data.data.records;
           if (engagements.value&&engagements.value.length>0){
-            if (currentEngagementIndex.value == 0){
-              currentEngagement.value =  engagements.value[0];
+            // 如果路由中有学生ID参数,尝试定位到该学生
+            if (studentIdFromRoute) {
+              const targetIndex = engagements.value.findIndex((engagement:any) => 
+                engagement.studentId === studentIdFromRoute
+              )
+              if (targetIndex !== -1) {
+                currentEngagementIndex.value = targetIndex
+                console.log("从路由参数定位到学生,索引:", targetIndex)
+              } else {
+                currentEngagementIndex.value = 0
+                console.log("未找到指定学生,默认选择第一个")
+              }
+            } else {
+              if (currentEngagementIndex.value == 0){
+                currentEngagement.value =  engagements.value[0];
+              }
+              currentEngagementIndex.value = 0;
             }
-            currentEngagementIndex.value = 0;
           }
           else{
             currentEngagementIndex.value = -1;

+ 58 - 31
src/views/CorrectPage/CorrectPage.vue

@@ -49,8 +49,8 @@
   
     <!-- 右侧内容区域 -->
     <div class="right-content">
-      <!-- 作业描述区域 -->
-      <div class="content-panel">
+      <!-- 作业描述区域 - 仅学生可见 -->
+      <div v-if="isStudent" class="content-panel">
         <!-- 上半部分:作业描述 -->
         <div class="assignment-section">
           <div class="section-header">
@@ -226,30 +226,33 @@
     </div>
     
     <!-- 行为记录对话框 -->
-        <el-dialog v-model="showDialog" title="文件信息">
-
-      <el-table :data="fileList" stripe>
-        <el-table-column prop="id" label="ID"></el-table-column>
-        <el-table-column prop="assignmentId" label="作业 ID"></el-table-column>
-        <el-table-column prop="recordFileUrl" label="文件链接 (复制粘贴到浏览器中打开)" width="300px">
+    <el-dialog 
+      v-model="showDialog" 
+      title="文件信息"
+      :width="dialogWidth"
+      class="behavior-dialog"
+    >
+      <el-table :data="fileList" stripe class="behavior-table">
+        <el-table-column prop="id" label="ID" min-width="60"></el-table-column>
+        <el-table-column prop="assignmentId" label="作业 ID" min-width="80"></el-table-column>
+        <el-table-column prop="recordFileUrl" label="文件链接" min-width="200">
           <template #default="scope">
-            <!-- <a :href="scope.row.recordFileUrl" target="_blank">{{ scope.row.recordFileUrl }}</a> -->
-            <span>{{ scope.row.recordFileUrl }}</span>
+            <span class="file-url">{{ scope.row.recordFileUrl }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="createdAt" label="时间">
+        <el-table-column prop="createdAt" label="时间" min-width="150">
           <template #default="scope">
-            {{ formatDateTime(scope.row.createdAt) }} <!-- 调用格式化函数 -->
+            {{ formatDateTime(scope.row.createdAt) }}
           </template>
         </el-table-column>
-        <el-table-column prop="studentId" label="学生 ID"></el-table-column>
+        <el-table-column prop="studentId" label="学生 ID" min-width="80"></el-table-column>
       </el-table>
 
       <template #footer>
-      <el-button @click="showDialog = false">取消</el-button>
-      <el-button type="primary" @click="downloadAllFiles">导出</el-button>
+        <el-button @click="showDialog = false">取消</el-button>
+        <el-button type="primary" @click="downloadAllFiles">导出</el-button>
       </template>
-      </el-dialog>
+    </el-dialog>
       
     <!-- 评语模板对话框 -->
     <el-dialog
@@ -379,6 +382,11 @@ const studentAnswer = ref('')
 // 课程ID(用于返回时保持菜单选中状态)
 const courseId = ref(0)
 
+// 响应式对话框宽度
+const dialogWidth = computed(() => {
+  return '50%'
+})
+
 // 格式化消息数据用于显示
 const formattedMessages = ref<IDialogue[]>([])
 
@@ -394,11 +402,20 @@ const updateFormattedMessages = () => {
 // 获取学生信息
 async function fetchStudentInfo() {
   try {
-    const response = await apis.findUserById(studentId)
-    if (response.data.code === 200) {
-      const userData = response.data.data.records[0]
+    // 获取学生基本信息(姓名)
+    const userResponse = await apis.findUserById(studentId)
+    if (userResponse.data.code === 200) {
+      const userData = userResponse.data.data.records[0]
       studentInfo.name = userData.name
-      studentInfo.className = userData.className || ''
+    }
+    
+    // 获取学生班级信息(需要先获取课程ID)
+    if (courseId.value) {
+      const classResponse = await apis.getClassNameOfStudent(courseId.value, studentId)
+      if (classResponse.data.code === 200) {
+        const classData = classResponse.data.data
+        studentInfo.className = classData.className || ''
+      }
     }
   } catch (error) {
     console.error('获取学生信息失败:', error)
@@ -463,26 +480,33 @@ async function fetchData() {
       router.go(-1);
     })
   
-  // 获取学生信息
-  await fetchStudentInfo()
-  
   // 获取现有的评分和评语
   await fetchCorrectionData()
   
-  // 获取作业详情(获取课程ID)
+  // 获取作业详情(获取课程ID)- 必须先获取,因为获取班级信息需要用到
   await fetchAssignmentDetails()
   
+  // 获取学生信息(包括班级信息)
+  await fetchStudentInfo()
+  
   // 获取批改进度统计
   await fetchCorrectionStats()
 }
 
-// 弹窗相关函数
+// 跳转到行为数据页面
 const handleBehaviorRecord = () => {
-  showDialog.value = true
-  // 如果数据为空,重新获取
-  if (fileList.length === 0) {
-    getBehaviorRecords()
-  }
+  // 跳转到行为数据分析页面,并携带课程ID、作业ID和学生ID参数
+  router.push({
+    path: '/home/behavior/main',
+    query: {
+      courseId: courseId.value.toString(),
+      assignmentId: assignmentId.toString(),
+      studentId: studentId.toString()
+    }
+  }).then(() => {
+      // 强制刷新页面以重新加载数据
+      window.location.reload();
+    });
 }
 
 // 获取行为记录数据
@@ -693,7 +717,10 @@ const goBackStudent = () => {
       courseId: courseId.value.toString(),
       fromAssignment: 'true' // 标记是从作业页面返回
     }
-  });
+  }).then(() => {
+      // 强制刷新页面以重新加载数据
+      window.location.reload();
+    });
 };
 
 onMounted(() => {

Datei-Diff unterdrückt, da er zu groß ist
+ 548 - 98
src/views/CorrectPage/CoursePage.vue


+ 205 - 69
src/views/CorrectPage/index.scss

@@ -9,13 +9,16 @@
   background-color: #FFF;
   border-radius: 15px;
   height: 125px;
-  width: 330px;
-  margin-top: 0px;
-  margin-left: 0px;
+  width: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
-  position: relative; // 为绝对定位的错误提示提供相对定位容器
+  position: relative;
+  flex-shrink: 0;
+  
+  @media (max-width: 1439px) {
+    height: 110px;
+  }
   
   .rating-component {
     display: flex;
@@ -112,46 +115,46 @@
 
     .error-message {
       position: absolute;
-      top: 90%; // 位于评分输入框下方
+      top: 90%;
       left: 60%;
-      transform: translateX(-50%); // 水平居中
+      transform: translateX(-50%);
       color: #ff4d4f;
       font-size: 14px;
-      font-weight: 700;
+      font-weight: 500;
       text-align: center;
-      white-space: nowrap; // 防止文字换行
+      white-space: nowrap;
       margin-top: 10px;
-      font-size: 16px;
-      font-weight: 500;
-      z-index: 10; // 确保在其他元素之上
+      z-index: 10;
     }
   }
 }
 
 .remark-section {
-  margin-top: 0px;
-  width: 330px;
+  width: 100%;
   background-color: #FFF;
-  height: 470px;
   border-radius: 15px;
-  padding: 15px 15px;
+  padding: 15px;
   box-sizing: border-box;
   display: flex;
   flex-direction: column;
+  flex: 1;
+  min-height: 0;
   
   .textarea-container {
     flex: 1;
+    display: flex;
+    min-height: 0;
     
     .remark-textarea {
-      width: 300px;
-      height: 445px;
+      width: 100%;
+      height: 100%;
       box-sizing: border-box;
       padding: 15px;
       border: 1px solid #e0e0e0;
       border-radius: 8px;
       font-size: 16px;
       line-height: 1.6;
-      resize: none; // 禁止用户调整大小
+      resize: none;
       font-family: inherit;
       background-color: #FFF;
       
@@ -168,15 +171,13 @@
       }
     }
   }
-  
-
 }
 
 // 按钮区域样式
 .button-section {
-  margin-top: 0px;
-  width: 330px;
-  height: 140px;
+  width: 100%;
+  height: auto;
+  min-height: 140px;
   background-color: #FFF;
   border-radius: 15px;
   padding: 15px 10px;
@@ -184,6 +185,11 @@
   display: flex;
   flex-direction: column;
   gap: 10px;
+  flex-shrink: 0;
+  
+  @media (max-width: 1439px) {
+    min-height: 130px;
+  }
   
   .button-row {
     display: flex;
@@ -263,6 +269,10 @@
   .progress-section {
     margin: 0px 0 0px 40px;
     
+    @media (max-width: 1439px) {
+      margin: 0px 0 0px 30px;
+    }
+    
     .progress-bar {
       :deep(.el-progress-bar__outer) {
         border-radius: 10px;
@@ -286,14 +296,23 @@
 
 // 学生功能区域样式
 .student-function-area {
-  margin-left: 20px;
-  margin-top: 25px;
+  margin: 10px 25px 10px 0;
   display: flex;
   flex-direction: column;
   gap: 20px;
+  flex-shrink: 0;
+  width: 330px;
+  height: calc(100vh - 20px);
+  
+  @media (max-width: 1439px) {
+    width: 280px;
+    margin: 10px 15px 10px 0;
+    gap: 15px;
+    height: calc(100vh - 20px);
+  }
   
   .student-score-display {
-    width: 330px;
+    width: 100%;
     background-color: #FFF;
     border-radius: 15px;
     height: 125px;
@@ -301,6 +320,11 @@
     align-items: center;
     justify-content: center;
     gap: 5px;
+    flex-shrink: 0;
+    
+    @media (max-width: 1439px) {
+      height: 110px;
+    }
     
     .score-label {
       font-size: 20px;
@@ -330,17 +354,17 @@
   }
   
   .student-remark-display {
-    width: 330px;
+    width: 100%;
     background-color: #FFF;
     border-radius: 15px;
-    height: 580px;
     padding: 20px;
     box-sizing: border-box;
+    flex: 1;
+    min-height: 0;
     
     .remark-content {
       width: 100%;
       height: 100%;
-      min-height: 260px;
       font-size: 16px;
       line-height: 1.6;
       color: #333;
@@ -364,13 +388,13 @@
   }
   
   .student-button-area {
-    width: 330px;
+    width: 100%;
     display: flex;
     justify-content: center;
-    margin-top: 0px;
+    flex-shrink: 0;
     
     .student-back-btn {
-      width: 330px;
+      width: 100%;
       height: 50px;
       background: linear-gradient(135deg, #5397F8, #4472C4);
       color: white;
@@ -381,6 +405,11 @@
       cursor: pointer;
       transition: all 0.3s ease;
       
+      @media (max-width: 1439px) {
+        height: 45px;
+        font-size: 17px;
+      }
+      
       &:hover {
         background: linear-gradient(135deg, #4585C6, #365F91);
         transform: translateY(-2px);
@@ -406,15 +435,17 @@
   flex-direction: column;
   gap: 15px;
   padding: 0 24px;
+  max-height: 60vh;
+  overflow-y: auto;
 
   .template-item {
     box-sizing: border-box;
-    padding: 0 16px;
+    padding: 12px 16px;
     border: 1px solid #e0e0e0;
-    border-radius: 4px;
+    border-radius: 8px;
     cursor: pointer;
     transition: all 0.3s ease;
-    height: 50px;
+    min-height: 50px;
     overflow: hidden;
     display: flex;
     align-items: center;
@@ -424,12 +455,12 @@
     &:hover {
       background-color: #f5f7fa;
       border-color: #597D3B;
+      transform: translateX(4px);
     }
 
     .template-content {
       width: 100%;
       text-align: left;
-      white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
       font-size: 14px;
@@ -442,10 +473,32 @@
 
 .correct-page {
   width: 100%;
-  height: 95vh;
+  height: 100vh;
   background-color: #f5f5f5;
   font-family: "Microsoft YaHei", sans-serif;
   display: flex;
+  flex-wrap: nowrap;
+  padding: 0;
+  box-sizing: border-box;
+  position: relative;
+  overflow: hidden;
+  
+  // 响应式断点定义(仅针对电脑屏幕)
+  // Large Desktop: > 1920px
+  // Desktop: 1440px - 1920px
+  // Laptop: 1024px - 1439px
+  
+  // 针对大屏幕的优化
+  @media (min-width: 1920px) {
+    .ai-chat-panel {
+      width: 420px;
+    }
+    
+    .teacher-function-area,
+    .student-function-area {
+      width: 360px;
+    }
+  }
 
   // AI对话面板样式
   .ai-chat-panel {
@@ -454,18 +507,27 @@
     border-right: 1px solid #e9ecef;
     display: flex;
     flex-direction: column;
-    margin-top: 25px;
-    margin-left: 25px;
+    margin: 10px 0 10px 25px;
     border-radius: 15px;
-    height: 90vh;
+    height: calc(100vh - 20px);
+    flex-shrink: 0;
+    overflow: hidden;
+    
+    // 笔记本屏幕
+    @media (max-width: 1439px) {
+      width: 320px;
+      margin: 10px 0 10px 15px;
+      height: calc(100vh - 20px);
+    }
 
-        .chat-header {
-        padding: 20px 16px 16px;
-        border-bottom: 1px solid #e9ecef;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        border-radius: 15px 15px 0 0; // 顶部圆角匹配面板
+    .chat-header {
+      padding: 20px 16px 16px;
+      border-bottom: 1px solid #e9ecef;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      border-radius: 15px 15px 0 0;
+      flex-shrink: 0;
 
       .chat-title {
         font-size: 18px;
@@ -500,7 +562,7 @@
     .chat-messages {
       flex: 1;
       overflow-y: auto;
-      padding: 16px 16px 20px 16px; // 增加底部padding为20px
+      padding: 16px 16px 20px 16px;
       display: flex;
       flex-direction: column;
       gap: 16px;
@@ -512,7 +574,7 @@
       
       &::-webkit-scrollbar-track {
         background: transparent;
-        margin: 8px 0; // 上下留白
+        margin: 8px 0;
       }
       
       &::-webkit-scrollbar-thumb {
@@ -524,7 +586,6 @@
         }
       }
       
-      // 确保滚动条不会影响圆角
       border-radius: 0 0 15px 15px;
 
       .message-group {
@@ -569,9 +630,9 @@
           }
 
           .user-bubble {
-            background: #5cb3f5;
+            background: linear-gradient(135deg, #4ca1f4, #5cb3f5, #6ec0f6);;
             color: white;
-            border-bottom-right-radius: 6px;
+            border-radius: 20px;
           }
         }
 
@@ -579,7 +640,7 @@
           width: 32px;
           height: 32px;
           border-radius: 16px;
-          background: #5cb3f5;
+          background: linear-gradient(135deg, #4ca1f4, #5cb3f5, #6ec0f6);
           display: flex;
           align-items: center;
           justify-content: center;
@@ -600,7 +661,7 @@
           width: 32px;
           height: 32px;
           border-radius: 16px;
-          background: #6c757d;
+          background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
           display: flex;
           align-items: center;
           justify-content: center;
@@ -632,9 +693,9 @@
           }
 
           .ai-bubble {
-            background: #e9ecef;
+            background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
             color: #2c3e50;
-            border-bottom-left-radius: 6px;
+            border-radius: 20px;
                 }
     }
   }
@@ -854,18 +915,29 @@
   .right-content {
     display: flex;
     flex-direction: column;
-    margin-left: 30px;
-    margin-top: 25px;
+    margin: 10px 20px 10px 30px;
     gap: 20px;
+    flex: 1;
+    min-width: 0;
+    height: calc(100vh - 20px);
+    
+    // 笔记本屏幕
+    @media (max-width: 1439px) {
+      margin: 10px 15px 10px 20px;
+      gap: 15px;
+      height: calc(100vh - 20px);
+    }
   }
 
   // 教师功能区域
   .teacher-function-area {
-    margin-left: 20px;
-    margin-top: 25px;
+    margin: 10px 25px 10px 0;
     display: flex;
     flex-direction: column;
     gap: 15px;
+    flex-shrink: 0;
+    width: 330px;
+    height: calc(100vh - 20px);
 
     // 查看行为记录按钮
     .behavior-record-btn {
@@ -877,40 +949,52 @@
       font-weight: 600;
       border-radius: 12px;
       white-space: nowrap;
-      margin-left: 20px;
-      width: fit-content;
+      width: 100%;
       height: 40px;
-      line-height: 40px;
-      width: 300px;
       
       &:hover {
         background: #6BA3D0;
         border-color: #6BA3D0;
       }
     }
+    
+    // 笔记本屏幕
+    @media (max-width: 1439px) {
+      width: 280px;
+      margin: 10px 15px 10px 0;
+      gap: 12px;
+      height: calc(100vh - 20px);
+    }
   }
 
-  // 作业描述面板样式
+  // 作业描述面板样式(仅学生可见)
   .content-panel {
-    width: 900px;
+    width: 100%;
     height: 240px;
     background: #FFF;
     border-radius: 15px;
     border: 1px solid #e9ecef;
     display: flex;
     flex-direction: column;
-    overflow: hidden;
+    flex-shrink: 0;
+    
+    // 笔记本屏幕
+    @media (max-width: 1439px) {
+      height: 220px;
+    }
 
     .assignment-section {
       flex: 1;
       border-bottom: 1px solid #e9ecef;
       display: flex;
       flex-direction: column;
+      min-height: 0;
 
       .section-header {
         padding: 16px 20px 12px;
         border-bottom: 1px solid #f0f0f0;
         background: #FFF;
+        flex-shrink: 0;
 
         h2 {
           margin: 0;
@@ -925,14 +1009,35 @@
         padding: 16px 20px;
         overflow-y: auto;
         background: #fff;
+        min-height: 0;
+        
+        // 自定义滚动条
+        &::-webkit-scrollbar {
+          width: 6px;
+        }
+        
+        &::-webkit-scrollbar-track {
+          background: transparent;
+          margin: 8px 0;
+        }
+        
+        &::-webkit-scrollbar-thumb {
+          background: #d1d5db;
+          border-radius: 3px;
+          
+          &:hover {
+            background: #9ca3af;
+          }
+        }
       }
     }
   }
 
   // 学生信息和回答区域样式
   .student-info-section {
-    width: 900px;
-    height: 550px;
+    width: 100%;
+    flex: 1;
+    min-height: 0;
     background: #FFF;
     border-radius: 15px;
     border: 1px solid #e9ecef;
@@ -947,11 +1052,14 @@
       display: flex;
       align-items: center;
       gap: 20px;
+      flex-wrap: wrap;
+      flex-shrink: 0;
 
       .student-label, .class-label {
         font-weight: 600;
         color: #333;
         font-size: 14px;
+        white-space: nowrap;
       }
 
       .student-name, .class-name {
@@ -976,6 +1084,7 @@
         text-align: center;
         font-family: "Noto Sans SC", sans-serif;
         margin-left: 10px;
+        white-space: nowrap;
       }
     }
 
@@ -989,6 +1098,20 @@
       color: #333;
       white-space: pre-wrap;
       word-wrap: break-word;
+      
+      // 自定义滚动条
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+      
+      &::-webkit-scrollbar-thumb {
+        background: #d1d5db;
+        border-radius: 3px;
+        
+        &:hover {
+          background: #9ca3af;
+        }
+      }
     }
   }
 }
@@ -1051,4 +1174,17 @@
         right: 28vh;
       }
     }
+}
+
+// 行为记录对话框样式
+.behavior-dialog {
+  :deep(.el-dialog__body) {
+    padding: 20px;
+    overflow-x: auto;
+  }
+  
+  .file-url {
+    word-break: break-all;
+    font-size: 14px;
+  }
 }

+ 3 - 2
src/views/CoursePage/CoursePage.vue

@@ -7,10 +7,11 @@
   <el-card style="margin: 10px auto 0;height: 100%; width: 80%" shadow="never">
     <el-button :icon="Refresh" circle @click="doRefresh"></el-button>
     <template v-if="role == 'teachers'">
-      <el-button @click="visible = !visible" color="#597d3b">新增课程</el-button>
+      <el-button @click="visible = !visible" style="background-color: #409EFF; border-color: #409EFF; color: white;">新增课程</el-button>
     </template>
     <el-input v-model="courseName" placeholder="请输入课程名称" clearable style="width: 180px; margin: 0 10px 0 10px ;"/>
-    <el-button color="#597D3B" @click="handleSearch">
+    <el-button   style="background-color: #409EFF; border-color: #409EFF; color: white;" 
+ @click="handleSearch">
       <el-icon style="margin-right: 3px"><Search/></el-icon>
       查询
     </el-button>

+ 1 - 1
src/views/CourseSquare/component/AllCourse/MoreCourse.vue

@@ -7,7 +7,7 @@
       <div class="course-content-title">
         <el-button :icon="Refresh" circle @click="doRefresh" style="margin: 0 10px 0 30px"></el-button>
         <el-input v-model="courseName" placeholder="请输入课程名称" clearable class="search-input" style="width: 180px; margin-right: 10px"/>
-        <el-button @click="handleSearch" color="#597D3B">
+        <el-button style="background-color: #409EFF; border-color: #409EFF; color: white;"  @click="handleSearch">
           <el-icon style="margin-right: 3px" ><Search/></el-icon>
           查询
         </el-button>

+ 9 - 12
src/views/CourseSquare/component/CourseDetails/component/CourseHomeworkTable.vue

@@ -6,7 +6,7 @@
 <template>
   <el-card>
     <template v-if="props.role == 'teachers'">
-      <el-button color="#597d3b" @click="isTypeDialogVisible=true" v-if="props.isShowNewHomework">新建作业</el-button>
+      <el-button style="background-color: #6A9BD8; color: white;" @click="isTypeDialogVisible=true" v-if="props.isShowNewHomework">新建作业</el-button>
     </template>
     <template v-if="props.tableData.length > 0">
       <el-table :data="props.tableData" stripe style="width: 100%" height="100%" show-overflow-tooltip>
@@ -22,7 +22,7 @@
         <!-- TODO:找到了 详情界面 -->
         <el-table-column label="详情" width="60">
           <template #default="scope">
-            <el-button color="#597d3b" circle @click="openEditor(scope.row.descriptionFile, scope.row.assignmentName)">
+            <el-button   style="background-color: #6A9BD8; color: white;" circle @click="openEditor(scope.row.descriptionFile, scope.row.assignmentName)">
               <el-icon :size="18">
                 <Search />
               </el-icon>
@@ -32,7 +32,7 @@
         <el-table-column :label="role == 'teachers'? '编辑':'开始'" width="60">
           <template #default="scope">
             <el-button @mouseover="handleMouse('over', scope.row.teacherId, scope.row.endDate)" @mouseleave="handleMouse('leave', scope.row.teacherId, scope.row.endDate)"
-                color="#597d3b" circle @click="handleFunctionButton(scope.row.assignmentId,scope.row.type)" :disabled="props.disabled || isDisabled(scope.row.teacherId, scope.row.endDate)">
+                style="background-color: #6A9BD8; color: white;" circle @click="handleFunctionButton(scope.row.assignmentId,scope.row.type)" :disabled="props.disabled || isDisabled(scope.row.teacherId, scope.row.endDate)">
               <el-icon :size="18">
                 <Edit />
               </el-icon>
@@ -70,7 +70,7 @@
     </el-radio-group>
     <div class="dialog-footer" style="margin-top:40px;margin-left:30%;">
       <el-button @click="isTypeDialogVisible = false">取消</el-button>
-      <el-button color="#597d3b" type="primary" @click="createAssignmentDialog()">确定</el-button>
+      <el-button style="background-color: #6A9BD8; color: white;" type="primary" @click="createAssignmentDialog()">确定</el-button>
     </div>
   </el-dialog>
   <ViewsWordEditor :alert="alertProps" :docxInfo="docxInfo"></ViewsWordEditor>
@@ -257,11 +257,7 @@ const openEditor = (url: string, title: string) => {
        //如果被禁用则提示
       if((props.disabled || isDisabled(teacherId, endDate))&&throttleMessage()){
         if(props.role == 'student'){
-          if(props.disabled){
-            ElMessage.warning('请先加入课程')
-          } else {
-            ElMessage.warning('作业已经超过截止时间')
-          }
+          ElMessage.warning('请先加入课程')
         }else if(props.role == 'teachers'){
           ElMessage.warning('无权限')
         }
@@ -287,7 +283,9 @@ const openEditor = (url: string, title: string) => {
   }
 
   const handleWriting = (assignmentId:number) => {
-    router.push(`/home/homeworkDetail/${assignmentId}?fromCourse=1`);
+    router.push(`/home/homeworkDetail/${assignmentId}?fromCourse=1`).then(() => {
+      window.location.reload()
+    });
     // Location.reload()
   }
 
@@ -299,9 +297,8 @@ const openEditor = (url: string, title: string) => {
       if(+teacherId != store.user.id){
         return true
       }
-    } else { // 是学生且超时则禁用
-      if(new Date() > new Date(endDate)) return true
     }
+    // 学生不再因为超过截止日期而禁用按钮
     return false
   }
 

+ 15 - 6
src/views/CourseSquare/component/CourseDetails/index.vue

@@ -28,11 +28,11 @@
         <div>
           <template v-if="showCode && role == 'student'">
             <el-input v-model="enrollCode" placeholder="输入邀请码" type="text" style="width: 150px;margin-right: 20px"></el-input>
-            <el-button color="#597d3b" @click="enroll">加入课程</el-button>
+            <el-button @click="enroll" style="background-color: #409EFF; border-color: #409EFF; color: white;" >加入课程</el-button>
           </template>
           <template v-if="showEdit && role == 'teachers'">
-            <el-button color="#597d3b" @click="visible = true; fileList[0] = {name: '壁纸', url: newCourseInfo.courseImages[0]}">编辑课程</el-button>
-            <el-button color="#4a90c4" style="color: #fff" @click="$router.push(`/home/courseClass/${courseId}`)">班级管理</el-button>
+            <el-button style="background-color: #4a90c4 ;color: white" @click="visible = true; fileList.value = newCourseInfo.courseImages[0] ? [{ name: '壁纸', url: newCourseInfo.courseImages[0], uid: Date.now(), status: 'success' }] : []">编辑课程</el-button>
+            <el-button  style="background-color: #4a90c4 ;color: white" @click="$router.push(`/home/courseClass/${courseId}`)">班级管理</el-button>
           </template>
         </div>
       </div>
@@ -95,6 +95,7 @@
             :on-error="handleError"
             :on-exceed="handleExceed"
             :on-change="handleChange"
+            :on-remove="handleRemove"
             list-type="picture-card"
             accept=".png, .jpg"
             :auto-upload="false"
@@ -124,7 +125,7 @@
 </template>
 
 <script setup lang="ts">
-import {onMounted, reactive, ref} from 'vue';
+import {onMounted, reactive, ref, nextTick} from 'vue';
 import {
   ElTabs,
   ElTabPane,
@@ -291,6 +292,14 @@ const handleError: UploadProps['onError'] = (error: Error, uploadFile: UploadFil
   console.log(error, uploadFile, uploadFiles)
 }
 
+const handleRemove: UploadProps['onRemove'] = async (uploadFile: UploadFile, uploadFiles: UploadFiles) => {
+  // 删除图片后,将图片设置为空字符串,后端会处理为空
+  newCourseInfo.courseImages[0] = ''
+  await nextTick()
+  // 清空文件列表
+  fileList.value = []
+}
+
 const handleExceed = (files, fileList) => {
   // console.log(files)
   uploadRef.value.clearFiles()  //清空上传文件(限制一个,所以直接清空即可)
@@ -596,7 +605,7 @@ onMounted(() => {
 
 <style scoped>
 ::v-deep .el-tabs__item {
-  color:#597D3B;
+  color:#6A9BD8;
   opacity: 1;
 }
 
@@ -612,7 +621,7 @@ onMounted(() => {
 
 /* 下划线颜色 */
 ::v-deep .el-tabs__active-bar {
-  background-color: #597D3B;
+  background-color: #6A9BD8;
 }
 
 :deep(.el-upload-dragger) {

+ 5 - 0
src/views/EditPage/EditPage.vue

@@ -70,12 +70,17 @@
       )
           .then(() => {
             router.push(`/home/homeworkDetail/${assignmentId}`)
+          }).then(() => {
+            window.location.reload()
           })
           .catch(() => {
             return
           })
     }else {
       router.push(`/home/homeworkDetail/${assignmentId}`)
+      .then(() => {
+        window.location.reload()
+      })
     }
   }
 

+ 1 - 1
src/views/EditPage/index.scss

@@ -23,6 +23,6 @@
     width: 100%;
     flex: 1;
     max-width: 100%;
-
+    margin-right: -10px;
   }
 }

+ 9 - 0
src/views/Home/component/Dictionaries/index.scss

@@ -14,6 +14,15 @@
     flex: 1;
     padding: 15px;
     box-sizing: border-box;
+  }
 
+  .dic-result {
+    white-space: pre-wrap;
+    overflow-y: auto;
+    flex: 1;
+    margin-top: 10px;
+    padding: 5px;
+    word-wrap: break-word;
+    word-break: break-word;
   }
 }

+ 1 - 1
src/views/Home/component/Dictionaries/index.vue

@@ -23,7 +23,7 @@
           </el-button>
         </template>
       </el-input>
-      <div style="white-space: pre-wrap;">{{ res }}</div>
+      <div :class="$style['dic-result']">{{ res }}</div>
     </div>
   </div>
 </template>

+ 2 - 0
src/views/Home/component/Edit/index.vue

@@ -114,6 +114,8 @@
       console.log(props)
       const _res = await apis.engagementStage(store.user.id, props.engagement.assignmentId, JSON.stringify(getQuillContent), getText.value, getHTML.value);
       emitter.emit('click-stage');
+      // 通知 Silder 组件暂存成功,可以启用批改按钮
+      emitter.emit('stage-success');
       success(_res.data.data);
       return _res;
     } catch (error) {

+ 368 - 26
src/views/Home/component/Silder/index.scss

@@ -1,17 +1,21 @@
 .slider-content {
   width: 20%;
-  margin-left: 20px;
-  box-shadow: rgba(0, 0, 0, 0.1) 0 4px 10px;
+  min-width: 240px;
+  max-width: 350px;
+  margin-left: 1.25rem; /* 20px */
+  box-shadow: rgba(0, 0, 0, 0.1) 0 0.25rem 0.625rem;
   display: flex;
   flex-direction: column;
-  transition: all .3s;
-  padding: 20px 0;
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+  padding: 1.25rem 0; /* 20px 0 */
   box-sizing: border-box;
   flex-shrink: 0;
   background-color: #ffffff;
+  overflow: hidden;
 
   &.shrink {
     width: 80px;
+    min-width: 80px;
     align-items: center;
 
     & .slider-icons {
@@ -21,61 +25,131 @@
 
     & .slider-item-box {
       padding: 0;
-      margin-top: 30px;
+      margin-top: 1.875rem; /* 30px */
     }
   }
 
   & .slider-icons {
-    margin-left: 10px;
+    margin-left: 0.625rem; /* 10px */
     cursor: pointer;
+    transition: all 0.2s ease;
+    
+    &:hover {
+      transform: scale(1.05);
+    }
   }
 
   .slider-item-box {
     display: flex;
     flex-direction: column;
-    gap: 20px;
-    margin-top: 20px;
-    cursor: pointer;
-    padding: 0 15px 15px 15px;
+    gap: 1.25rem; /* 20px */
+    margin-top: 1.25rem; /* 20px */
+    padding: 0 0.9375rem 0.9375rem 0.9375rem; /* 0 15px 15px 15px */
     box-sizing: border-box;
     height: 100%;
+    overflow-y: auto;
+    scrollbar-width: thin;
+    scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
+  }
+  
+  /* Webkit浏览器滚动条美化 */
+  .slider-item-box::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  .slider-item-box::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  .slider-item-box::-webkit-scrollbar-thumb {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+  }
+
+  .slider-item-box::-webkit-scrollbar-thumb:hover {
+    background: rgba(0, 0, 0, 0.3);
   }
 
 
   .slider-tag {
     display: flex;
     flex-direction: column;
-    gap: 10px;
+    gap: 0.625rem; /* 10px */
 
     .slider-tag-item {
       background-color: #4A90C4;
       color: #FFFFFF;
-      padding: 8px 0;
+      padding: 0.5rem 0; /* 8px 0 */
       display: flex;
       justify-content: center;
-      font-size: 14px;
+      font-size: 0.875rem; /* 14px */
       border-radius: 1000px;
       cursor: pointer;
+      transition: all 0.2s ease;
+      white-space: nowrap;
+      
+      &:hover {
+        background-color: #3a7aa4;
+        transform: translateY(-2px);
+        box-shadow: 0 4px 8px rgba(74, 144, 196, 0.3);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
     }
   }
 
   .capacity-item {
     width: 100%;
-    height: 40vh;
-    background-color: #d9d9d9;
-    border-radius: 10px;
+    height: 48vh;
+    min-height: 300px;
+    max-height: 600px;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    border-radius: 0.75rem; /* 12px */
+    box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+
+    &:hover {
+      box-shadow: 0 0.375rem 1rem rgba(0, 0, 0, 0.12);
+    }
 
     .remark {
       box-sizing: border-box;
       width: 100%;
       height: 100%;
-      //max-height: 70vh;
-      border-radius: 10px;
-      overflow-y: auto; /* 添加垂直滚动 */
-      white-space: pre-wrap; /* 保留换行和空格,允许自动换行 */
-      //background-color: #f0f0f0;
-      padding: 10px;
-      //background-color: rgba(255, 75, 75, 0.3);
+      border-radius: 0.75rem; /* 12px */
+      overflow-y: auto;
+      overflow-x: hidden;
+      white-space: pre-wrap;
+      word-wrap: break-word;
+      word-break: break-word;
+      padding: 1rem; /* 16px */
+      line-height: 1.8;
+      color: #2c3e50;
+      font-size: 0.875rem; /* 14px */
+      background-color: rgba(255, 255, 255, 0.6);
+      backdrop-filter: blur(10px);
+      
+      /* 自定义滚动条样式 */
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+      
+      &::-webkit-scrollbar-track {
+        background: rgba(0, 0, 0, 0.05);
+        border-radius: 10px;
+      }
+      
+      &::-webkit-scrollbar-thumb {
+        background: rgba(74, 144, 196, 0.5);
+        border-radius: 10px;
+        transition: background 0.3s ease;
+        
+        &:hover {
+          background: rgba(74, 144, 196, 0.8);
+        }
+      }
     }
   }
 
@@ -83,17 +157,285 @@
 
 
 .slider-item {
-  font-size: 14px;
+  font-size: 0.875rem; /* 14px */
   width: 40px;
   height: 40px;
+  min-width: 40px;
+  min-height: 40px;
   display: flex;
   align-items: center;
   justify-content: center;
   background-color: #d9d9d9;
   border-radius: 100px;
-  box-shadow: rgba(0, 0, 0, 0.1) 0 4px 10px;
+  box-shadow: rgba(0, 0, 0, 0.1) 0 0.25rem 0.625rem;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  
+  &:hover {
+    background-color: #c0c0c0;
+    transform: scale(1.05);
+    box-shadow: rgba(0, 0, 0, 0.15) 0 0.375rem 0.875rem;
+  }
+  
+  &:active {
+    transform: scale(0.98);
+  }
 }
 
 .edit-text {
-  margin-left: 10px;
+  margin-left: 0.625rem; /* 10px */
+  font-size: 0.875rem; /* 14px */
+}
+
+/* 平板设备 (≤1440px) */
+@media (max-width: 1440px) {
+  .slider-content {
+    width: 22%;
+    min-width: 220px;
+    max-width: 320px;
+    margin-left: 1rem;
+    padding: 1rem 0;
+
+    .slider-item-box {
+      gap: 1rem;
+      margin-top: 1rem;
+      padding: 0 0.75rem 0.75rem 0.75rem;
+    }
+
+    .slider-tag .slider-tag-item {
+      font-size: 0.8125rem; /* 13px */
+      padding: 0.4rem 0;
+    }
+
+    .capacity-item {
+      height: 45vh;
+      min-height: 250px;
+
+      .remark {
+        font-size: 0.8125rem; /* 13px */
+        padding: 0.875rem;
+      }
+    }
+  }
+
+  .slider-item {
+    width: 36px;
+    height: 36px;
+    min-width: 36px;
+    min-height: 36px;
+    font-size: 0.8125rem;
+  }
+}
+
+/* 小屏幕 (≤1024px) */
+@media (max-width: 1024px) {
+  .slider-content {
+    width: 24%;
+    min-width: 200px;
+    max-width: 260px;
+    margin-left: 0.75rem;
+    padding: 0.875rem 0;
+
+    .slider-item-box {
+      gap: 0.75rem;
+      margin-top: 0.75rem;
+      padding: 0 0.625rem 0.625rem 0.625rem;
+    }
+
+    .slider-tag .slider-tag-item {
+      font-size: 0.75rem; /* 12px */
+      padding: 0.375rem 0;
+    }
+
+    .capacity-item {
+      height: 42vh;
+      min-height: 220px;
+
+      .remark {
+        font-size: 0.75rem; /* 12px */
+        padding: 0.75rem;
+        line-height: 1.6;
+      }
+    }
+  }
+
+  .slider-item {
+    width: 32px;
+    height: 32px;
+    min-width: 32px;
+    min-height: 32px;
+    font-size: 0.75rem;
+  }
+
+  .edit-text {
+    margin-left: 0.5rem;
+    font-size: 0.75rem;
+  }
+}
+
+/* 平板竖屏或小笔记本 (≤900px) */
+@media (max-width: 900px) {
+  .slider-content {
+    width: 100%;
+    max-width: 100%;
+    margin-left: 0;
+    margin-top: 0.5rem;
+    padding: 0.75rem;
+    order: 1; /* 移到底部 */
+
+    &.shrink {
+      width: 100%;
+      min-width: 100%;
+      
+      & .slider-item-box {
+        padding: 0.5rem;
+        margin-top: 0.75rem;
+      }
+    }
+
+    .slider-item-box {
+      flex-direction: row;
+      flex-wrap: wrap;
+      gap: 0.5rem;
+      margin-top: 0.5rem;
+      padding: 0.5rem;
+      max-height: 50vh;
+    }
+
+    .slider-tag {
+      flex-direction: row;
+      flex-wrap: wrap;
+      width: 100%;
+
+      .slider-tag-item {
+        flex: 0 1 auto;
+        padding: 0.375rem 0.75rem;
+        min-width: 80px;
+      }
+    }
+
+    .capacity-item {
+      width: 100%;
+      height: auto;
+      min-height: 200px;
+      max-height: 300px;
+
+      .remark {
+        font-size: 0.8125rem;
+      }
+    }
+  }
+}
+
+/* 移动设备 (≤768px) */
+@media (max-width: 768px) {
+  .slider-content {
+    padding: 0.5rem;
+
+    .slider-item-box {
+      gap: 0.375rem;
+      padding: 0.375rem;
+    }
+
+    .slider-tag .slider-tag-item {
+      font-size: 0.6875rem; /* 11px */
+      padding: 0.3rem 0.5rem;
+      min-width: 70px;
+    }
+
+    .capacity-item {
+      min-height: 180px;
+      max-height: 250px;
+
+      .remark {
+        font-size: 0.75rem;
+        padding: 0.625rem;
+      }
+    }
+  }
+
+  .slider-item {
+    width: 28px;
+    height: 28px;
+    min-width: 28px;
+    min-height: 28px;
+    font-size: 0.6875rem;
+  }
+}
+
+/* 超大屏幕 (≥2560px) */
+@media (min-width: 2560px) {
+  .slider-content {
+    width: 18%;
+    min-width: 320px;
+    max-width: 450px;
+    margin-left: 1.5rem;
+    padding: 1.5rem 0;
+
+    .slider-item-box {
+      gap: 1.5rem;
+      margin-top: 1.5rem;
+      padding: 0 1.25rem 1.25rem 1.25rem;
+    }
+
+    .slider-tag .slider-tag-item {
+      font-size: 1rem; /* 16px */
+      padding: 0.625rem 0;
+    }
+
+    .capacity-item {
+      height: 50vh;
+      min-height: 400px;
+      max-height: 800px;
+
+      .remark {
+        font-size: 1rem; /* 16px */
+        padding: 1.25rem;
+        line-height: 2;
+      }
+    }
+  }
+
+  .slider-item {
+    width: 48px;
+    height: 48px;
+    min-width: 48px;
+    min-height: 48px;
+    font-size: 1rem;
+  }
+
+  .edit-text {
+    margin-left: 0.75rem;
+    font-size: 1rem;
+  }
+}
+
+/* 高对比度模式 */
+@media (prefers-contrast: high) {
+  .slider-content {
+    border: 2px solid currentColor;
+  }
+
+  .slider-item,
+  .capacity-item {
+    border: 2px solid currentColor;
+  }
+}
+
+/* 减少动画模式 */
+@media (prefers-reduced-motion: reduce) {
+  .slider-content,
+  .slider-item,
+  .slider-tag-item {
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+    transition-duration: 0.01ms !important;
+  }
+}
+
+/* 打印样式 */
+@media print {
+  .slider-content {
+    display: none;
+  }
 }

+ 73 - 16
src/views/Home/component/Silder/index.vue

@@ -54,7 +54,7 @@
 </template>
 
 <script lang="ts" setup>
-import {defineProps, nextTick, onMounted, reactive, ref, watch, watchEffect} from 'vue'
+import {defineProps, nextTick, onMounted, onUnmounted, reactive, ref, watch, watchEffect} from 'vue'
   import "./index.scss"
 import type {engagementVO, IAssignment} from "@/types/vo";
   import useApis from "@/apis";
@@ -66,6 +66,7 @@ import {useRoute} from "vue-router";
 import {ElMessage} from "element-plus";
 import router from "@/router";
 import * as path from "node:path";
+import emitter from "@/utils/emitter";
 
 
   interface IPreviewProps {
@@ -86,6 +87,7 @@ import * as path from "node:path";
   let open= ref(false)
   let showCorrectBtn = ref(false)
   let showCorrectToPageBtn = ref(false)
+  let hasStagedContent = ref(false) // 用于跟踪是否已暂存内容
 
   // 获得路由中的assignmentId
   const route = useRoute()
@@ -128,10 +130,16 @@ import * as path from "node:path";
     });
   };
 
-  const getEvaluation = () => {
-    apis.getOverall(props.engagement.assignmentId, store.user.id,props.engagement.version)
+  const getEvaluation = (version?: number) => {
+    // 如果没有传入version,使用props中的version
+    const versionToUse = version !== undefined ? version : props.engagement.version;
+    
+    apis.getOverall(props.engagement.assignmentId, store.user.id, versionToUse)
         .then(res => {
-          data.overallEvaluation = res.data.data
+          if (res.data.data) {
+            data.overallEvaluation = res.data.data;
+            console.log('成功获取评语:', data.overallEvaluation);
+          }
           loading.value = false;
         })
         .catch(err => {
@@ -145,9 +153,39 @@ import * as path from "node:path";
     apis.overAllEvaluation(props.engagement.assignmentId, store.user.id)
         .then(res => {
           if(res.data.code === 200){
-            getEvaluation()
-            // 允许查看详细报告
-            showCorrectToPageBtn.value = true;
+            console.log('批改请求成功,等待后端处理完成...');
+            
+            // 延迟获取评语,确保后端已经保存,并获取最新版本号
+            setTimeout(() => {
+              // 先获取最新的版本列表
+              apis.getEvaluationVersionList(props.engagement.assignmentId, store.user.id)
+                  .then(versionRes => {
+                    if (versionRes.data.data && versionRes.data.data.length > 0) {
+                      // 获取最新版本号(列表第一个就是最新的)
+                      const latestVersion = versionRes.data.data[0].version;
+                      console.log('获取到最新版本号:', latestVersion);
+                      
+                      // 使用最新版本号获取评语
+                      getEvaluation(latestVersion);
+                      
+                      // 允许查看详细报告
+                      showCorrectToPageBtn.value = true;
+                    } else {
+                      // 如果获取不到版本列表,尝试使用当前版本+1
+                      console.log('未获取到版本列表,使用当前版本+1');
+                      const nextVersion = (props.engagement.version || 0) + 1;
+                      getEvaluation(nextVersion);
+                      showCorrectToPageBtn.value = true;
+                    }
+                  })
+                  .catch(err => {
+                    console.error('获取版本列表失败:', err);
+                    // 如果获取版本列表失败,尝试使用当前版本+1
+                    const nextVersion = (props.engagement.version || 0) + 1;
+                    getEvaluation(nextVersion);
+                    showCorrectToPageBtn.value = true;
+                  });
+            }, 1500); // 延迟1.5秒,确保后端处理完成
           }else{
             ElMessage.error("智能批改请求超时")
             loading.value = false;
@@ -162,13 +200,10 @@ import * as path from "node:path";
         .then(res => {
           if(res.data.code === 400){
             ElMessage.error(res.data.msg)
-          }else{
-            ElMessage.error("智能批改请求超时")
-            loading.value = false;
           }
         })
         .catch(err => {
-          ElMessage.error("智能批改请求异常")
+          ElMessage.error("句子批改请求异常")
           console.log(err)
         })
   }
@@ -183,7 +218,9 @@ import * as path from "node:path";
   const handleCorrectToPage = () => {
     router.push({
       path: `/home/assignment/${props.engagement.assignmentId}/AIEvaluation`,
-    })
+    }).then(() => {
+      window.location.reload()
+    });
   }
 
   const openEditor = (url: string, title: string) => {
@@ -217,15 +254,22 @@ import * as path from "node:path";
    */
   const getEvaluationVersionList = () => {
     if(JSON.stringify(props.engagement) !== '{}'){
+      // 检查是否已有暂存的作文内容
+      const hasContent = props.engagement.content && props.engagement.content.trim() !== '';
+      if (hasContent) {
+        hasStagedContent.value = true;
+      }
+      
       apis.getEvaluationVersionList(props.engagement.assignmentId, store.user.id)
           .then(res => {
             // 从未批改过
             if(res.data.data.length === 0){
-              showCorrectBtn.value = true
+              // 修改:从未批改过时,只有当已有暂存内容时才能启用批改按钮
+              showCorrectBtn.value = hasStagedContent.value
               showCorrectToPageBtn.value = false
             } else {
               showCorrectToPageBtn.value = true
-              showCorrectBtn.value = (res.data.data[0].version !== props.engagement.version)
+              showCorrectBtn.value = (res.data.data[0].version !== props.engagement.version) && hasStagedContent.value
             }
           })
           .catch(err => {
@@ -251,13 +295,13 @@ import * as path from "node:path";
   // 使用节流包装 handleCorrectTip
   const handleCorrectTip = throttle(() => {
     if(!showCorrectBtn.value) {
-      ElMessage.error("请更新作文内容并暂存后批改");
+      ElMessage.info("请更新作文内容并暂存后批改");
     }
   }, 2000); // 2秒的节流时间
 
   const handleCorrectToPageTip = throttle(() => {
     if(!showCorrectToPageBtn.value)
-      ElMessage.error("批改后查看报告")
+      ElMessage.info("批改后查看报告")
   },1000);
 
   // 监听props变化
@@ -267,9 +311,22 @@ import * as path from "node:path";
     }
   }, { immediate: true });
 
+  // 监听暂存成功事件
+  emitter.on('stage-success', () => {
+    console.log('收到暂存成功事件,启用批改按钮')
+    // 暂存成功后,标记已有暂存内容,并启用"开始批改"按钮
+    hasStagedContent.value = true
+    showCorrectBtn.value = true
+  })
+
   onMounted(() => {
     fetchData()
   });
+
+  onUnmounted(() => {
+    // 清理事件监听器
+    emitter.off('stage-success');
+  });
 </script>
 
 <script lang="ts">

+ 3 - 1
src/views/LoginPage/component/Login.vue

@@ -153,7 +153,9 @@
         store.setToken(data?.token)
         localStorage.setItem('Authorization', data?.token);
         localStorage.setItem('User', JSON.stringify(data));
-        router.push(`/home`)
+        router.push(`/home`).then(() => {
+          window.location.reload();
+        });
       } else if(_res.data.code === 400){
         ElMessage({message: '用户名或密码错误', type: 'error'})
       }

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.