瀏覽代碼

feat:优化逐句批改功能

lalala 4 月之前
父節點
當前提交
0a019ae1f6

+ 8 - 0
src/apis/evaluation.ts

@@ -47,6 +47,14 @@ const evaluationApis = {
             }
         })
     },
+    getCurrentVersionEvaluation(assignmentId: number, studentId: number){
+        return axiosInstance.get(`${EVALUATION_PREFIX}/current`, {
+            params: {
+                assignmentId,
+                studentId
+            }
+        })
+    },
 }
 
 export default evaluationApis

+ 3 - 0
src/components/QuillEditor/Editor.vue

@@ -86,6 +86,9 @@ import {ElMessage} from "element-plus";
     // 文本变化监控
     quill.on(Quill.events.TEXT_CHANGE, (...args) => {
       const [delta, oldDelta, source] = args;
+      if (source === 'user') {
+        emitter.emit('is-modified', true)
+      }
       const oldContent = processOps(args["1"].ops);
       const docLength = quill.getLength() - 1;
       const letterCount = (quill.getText().match(/[a-zA-Z\d\u4e00-\u9fa5]/g) || []).length;

+ 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' ];
+    const publicPaths = [ '/login', '/register', '/passwordChange', '/admin/login' ];
     // 1. 检查URL中的token
     const urlToken = new URLSearchParams(window.location.search).get('token');
     if (urlToken) {

+ 190 - 12
src/views/AIEvaluationPage/AIEvaluationPage.vue

@@ -79,8 +79,10 @@
                 </div>
                 <div class="evaluation-message">
                   <div class="assistant-name">SEEAI英语助手</div>
-                  <div class="message-content">
-                    {{ data.overallEvaluation?.evaluation }}
+                  <div
+                    class="message-content markdown-content"
+                    v-html="renderMarkdown(data.overallEvaluation?.evaluation || '')"
+                  >
                   </div>
                 </div>
               </div>
@@ -164,6 +166,8 @@
   import {useRoute} from "vue-router";
   import {useStore} from "@/store";
   import { ArrowRight, Loading } from "@element-plus/icons-vue";
+  import { marked } from "marked";
+  import * as DOMPurifyModule from "dompurify";
 
   interface recordVersion {
     id: number,
@@ -186,12 +190,47 @@
     sentenceEvaluationList: [],
     versionList: []
   })
-  const selectedVersion = ref(1.2)
+  const selectedVersion = ref<number>(1)
+  const currentVersion = ref<number | null>(null)
   const isLoadingSentence = ref(false) // 逐句评价加载状态
   const isLoadingOverall = ref(false) // 整体评价加载状态
   let sentenceLoadingTimer: NodeJS.Timeout | null = null // 逐句评价延迟加载计时器
   let sentencePollingInterval: NodeJS.Timeout | null = null // 逐句评价轮询计时器
 
+  const sanitizeHtml = (dirty: string) => {
+    const purifierModule = DOMPurifyModule as unknown as {
+      default?: { sanitize: (input: string) => string },
+      sanitize?: (input: string) => string
+    }
+    const purifier = purifierModule.default ?? purifierModule
+    return purifier.sanitize(dirty)
+  }
+
+  marked.setOptions({
+    gfm: true,
+    breaks: true
+  })
+
+  const normalizeMarkdown = (content: string) => {
+    return content
+      .split('\n')
+      .map((line) => {
+        const headingMatch = line.match(/^\s*(#{1,6})\s*(.*)$/)
+        if (!headingMatch) {
+          return line
+        }
+        const [, hashes, text] = headingMatch
+        return `${hashes} ${text.trim()}`
+      })
+      .join('\n')
+  }
+
+  const renderMarkdown = (content: string) => {
+    const normalizedContent = normalizeMarkdown(content ?? '')
+    const html = marked.parse(normalizedContent)
+    return sanitizeHtml(typeof html === 'string' ? html : '')
+  }
+
   const toEvaluation = () => {
     apis.overAllEvaluation(assignmentId, store.user.id)
         .then(res => {
@@ -263,6 +302,11 @@
 
   const getEvaluation = (version: number) => {
     selectedVersion.value = version
+    console.info('[EVAL][FE] getEvaluation start', {
+      assignmentId,
+      userId: store.user?.id,
+      version
+    })
     
     // 清除之前的计时器和轮询
     if (sentenceLoadingTimer) {
@@ -278,11 +322,27 @@
     isLoadingOverall.value = true
     apis.getOverall(assignmentId,store.user.id,version)
         .then(res => {
-          data.overallEvaluation = res.data.data
+          if (res.data.code === 200) {
+            data.overallEvaluation = res.data.data
+            console.info('[EVAL][FE] overall ok', {
+              version,
+              hasData: !!res.data.data,
+              evalLen: res.data.data?.evaluation?.length ?? -1
+            })
+          } else {
+            data.overallEvaluation = null
+            console.warn('[EVAL][FE] overall non-200', {
+              version,
+              code: res.data.code,
+              msg: res.data.msg
+            })
+            ElMessage.error(res.data.msg || '获取整体评价失败')
+          }
         })
         .catch(err => {
-          console.log(err)
-          data.overallEvaluation = {} as OverallEvaluation
+          console.error('[EVAL][FE] overall request failed', { version, err })
+          data.overallEvaluation = null
+          ElMessage.error('整体评价请求失败')
         })
         .finally(() => {
           isLoadingOverall.value = false
@@ -299,6 +359,7 @@
             if(responseData && responseData.length > 0) {
               data.sentenceEvaluationList = responseData
               isLoadingSentence.value = false
+              console.info('[EVAL][FE] sentence ok', { version, count: responseData.length })
               
               // 检查数据完整性
               const hasEvaluation = responseData.some(item => item.evaluation)
@@ -331,9 +392,14 @@
           // 从未批改过
           if(res.data.data.length === 0){
             data.versionList = []
+            if (currentVersion.value != null) {
+              getEvaluation(currentVersion.value)
+            }
           } else {
             data.versionList = res.data.data
-            getEvaluation(res.data.data[0].version)
+            // 优先展示“当前最新提交版本”,否则回落到列表第一项(最新批改版本)
+            const prefer = currentVersion.value ?? res.data.data[0].version
+            getEvaluation(prefer)
           }
         })
         .catch(err => {
@@ -341,6 +407,19 @@
         })
   }
 
+  const fetchCurrentVersion = async () => {
+    try {
+      const res = await apis.getCurrentVersionEvaluation(assignmentId, store.user.id)
+      if (res.data.code === 200 && res.data.data?.version) {
+        currentVersion.value = res.data.data.version
+        selectedVersion.value = res.data.data.version
+      }
+    } catch (e) {
+      // 后端未部署 current 接口/用户未参与等情况,继续走历史列表逻辑
+      console.warn('获取当前版本失败:', e)
+    }
+  }
+
   const getBackgroundColor = (sentenceEvaluation: SentenceEvaluation) => {
     if (sentenceEvaluation.id === highlightSentenceId.value) {
       switch (sentenceEvaluation.type) {
@@ -376,7 +455,9 @@
   onMounted(() => {
     // toEvaluation()
     // getEvaluation()
-    getEvaluationVersionList()
+    fetchCurrentVersion().finally(() => {
+      getEvaluationVersionList()
+    })
   })
 
   // 组件卸载时清理定时器
@@ -411,7 +492,7 @@ export default {
 
 /* 作文展示区域 */
 .essay-section {
-  flex: 0 0 40%;
+  flex: 0 0 32%;
   min-width: 300px;
   position: relative;
   display: flex;
@@ -493,9 +574,105 @@ export default {
   overflow-wrap: break-word; /* 确保内容不会溢出 */
 }
 
+.markdown-content :deep(p) {
+  margin: 0 0 8px;
+}
+
+.markdown-content {
+  font-size: 13px;
+  line-height: 1.5;
+}
+
+.markdown-content :deep(p:last-child) {
+  margin-bottom: 0;
+}
+
+.markdown-content :deep(ul),
+.markdown-content :deep(ol) {
+  margin: 8px 0;
+  padding-left: 20px;
+}
+
+.markdown-content :deep(li) {
+  margin: 4px 0;
+}
+
+.markdown-content :deep(h1),
+.markdown-content :deep(h2),
+.markdown-content :deep(h3),
+.markdown-content :deep(h4),
+.markdown-content :deep(h5),
+.markdown-content :deep(h6) {
+  margin: 8px 0;
+  line-height: 1.4;
+  font-weight: 600;
+}
+
+.markdown-content :deep(h1) { font-size: 1.2em; }
+.markdown-content :deep(h2) { font-size: 1.12em; }
+.markdown-content :deep(h3) { font-size: 1.06em; }
+.markdown-content :deep(h4),
+.markdown-content :deep(h5),
+.markdown-content :deep(h6) { font-size: 1em; }
+
+.markdown-content :deep(code) {
+  padding: 2px 6px;
+  border-radius: 6px;
+  background-color: rgba(0, 0, 0, 0.06);
+  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
+  font-size: 0.9em;
+}
+
+.markdown-content :deep(pre) {
+  overflow-x: auto;
+  margin: 8px 0;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background-color: rgba(0, 0, 0, 0.03);
+}
+
+.markdown-content :deep(pre code) {
+  padding: 0;
+  background: transparent;
+}
+
+.markdown-content :deep(blockquote) {
+  margin: 8px 0;
+  padding-left: 12px;
+  border-left: 3px solid rgba(0, 0, 0, 0.18);
+  color: #555;
+}
+
+.markdown-content :deep(a) {
+  color: #2563eb;
+  text-decoration: underline;
+}
+
+.markdown-content :deep(table) {
+  width: 100%;
+  border-collapse: collapse;
+  margin: 10px 0;
+  border: 1px solid #e5e7eb;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.markdown-content :deep(th),
+.markdown-content :deep(td) {
+  border: 1px solid #e5e7eb;
+  padding: 6px 8px;
+  text-align: left;
+  vertical-align: top;
+}
+
+.markdown-content :deep(th) {
+  background-color: #f8fafc;
+  font-weight: 600;
+}
+
 /* AI评价区域 */
 .evaluation-section {
-  flex: 0 0 40%;
+  flex: 0 0 48%;
   min-width: 300px;
   overflow: hidden;
 }
@@ -610,7 +787,8 @@ export default {
   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
   word-wrap: break-word;
   overflow-wrap: break-word;
-  line-height: 1.6;
+  line-height: 1.5;
+  font-size: 13px;
 }
 
 .sentence-evaluation-content {
@@ -883,7 +1061,7 @@ export default {
   
   .message-content {
     padding: 10px 12px;
-    font-size: 14px;
+    font-size: 13px;
   }
   
   .sentence-evaluation-content {

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

@@ -124,6 +124,7 @@
     try {
       const _res = await apis.engagementStage(store.user.id, assignmentId, JSON.stringify(getQuillContent), getText.value, getHTML.value);
       emitter.emit('click-stage');
+      emitter.emit('is-modified', false);
       emitter.emit('stage-success');
       ElMessage.success(_res.data.data);
       return _res;

+ 1 - 0
src/views/EditPage/userWritingRecord.ts

@@ -99,6 +99,7 @@ export const removeAllEventListeners = () => {
     emitter.off("click-ai-send-button");
     emitter.off("is-modified");
     emitter.off("click-stage");
+    isModified.value = false;
 
     if (_visibilityChangeHandler) {
         document.removeEventListener('visibilitychange', _visibilityChangeHandler);

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

@@ -129,6 +129,7 @@
       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');
+      emitter.emit('is-modified', false);
       // 通知 Silder 组件暂存成功,可以启用批改按钮
       emitter.emit('stage-success');
       success(_res.data.data);

+ 23 - 0
src/views/Home/component/Silder/index.scss

@@ -167,9 +167,32 @@
           background: rgba(74, 144, 196, 0.8);
         }
       }
+
+      .remark-empty {
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #6b7280;
+        text-align: center;
+        padding: 0 0.75rem;
+      }
     }
   }
 
+  .correct-rule-tip {
+    margin-top: -0.5rem;
+    margin-bottom: 0.25rem;
+    padding: 0.5rem 0.625rem;
+    border-left: 4px solid #409eff;
+    border-radius: 0.5rem;
+    background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
+    color: #1d4ed8;
+    font-size: 0.75rem;
+    line-height: 1.45;
+    font-weight: 500;
+  }
+
 }
 
 

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

@@ -57,10 +57,28 @@
         <div>智能批改</div> 
         <div class="edit-box-header">
           <el-button @click="handleCorrect" :disabled="!showCorrectBtn" @mouseover="handleCorrectTip">开始批改</el-button>
-          <el-button @click="handleCorrectToPage" :disabled="!showCorrectToPageBtn" @mouseenter="handleCorrectToPageTip">查看完整批改报告</el-button>
+          <el-button
+            v-if="showCorrectToPageBtn"
+            @click="handleCorrectToPage"
+          >
+            查看过往批改记录
+          </el-button>
+        </div>
+        <div class="correct-rule-tip">
+          提示:建议在提交前使用逐句批改,同一版本仅可批改一次。
         </div>
         <div class="capacity-item">
-          <div v-loading="loading" element-loading-text="智能批改中" class="remark">{{ data.overallEvaluation?.evaluation }}</div>
+          <div v-loading="loading" element-loading-text="智能批改中" class="remark">
+            <div v-if="data.overallEvaluation?.evaluation">
+              {{ data.overallEvaluation.evaluation }}
+            </div>
+            <div v-else class="remark-empty">
+              <el-empty
+                :image-size="96"
+                description="暂无批改结果,点击“开始批改”后在此查看智能批改内容。"
+              />
+            </div>
+          </div>
         </div>
       </template>
     </div>
@@ -80,7 +98,6 @@ import ViewsWordEditor from "@/components/ViewsWordEditor/ViewsWordEditor.vue";
 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";
 
 
@@ -101,8 +118,11 @@ import emitter from "@/utils/emitter";
   let loading = ref(false)
   let open= ref(false)
   let showCorrectBtn = ref(false)
+  let canCorrectByVersion = ref(false)
   let showCorrectToPageBtn = ref(false)
   let hasStagedContent = ref(false) // 用于跟踪是否已暂存内容
+  let hasUnsavedChanges = ref(false) // 用于跟踪是否有未保存改动
+  let manualCorrectRequested = ref(false) // 仅允许手动点击后触发批改
 
   // 获得路由中的assignmentId
   const route = useRoute()
@@ -149,60 +169,79 @@ import emitter from "@/utils/emitter";
     // 如果没有传入version,使用props中的version
     const versionToUse = version !== undefined ? version : props.engagement.version;
     
-    apis.getOverall(props.engagement.assignmentId, store.user.id, versionToUse)
+    return apis.getOverall(props.engagement.assignmentId, store.user.id, versionToUse)
         .then(res => {
           if (res.data.data) {
             data.overallEvaluation = res.data.data;
             console.log('成功获取评语:', data.overallEvaluation);
           }
           loading.value = false;
+          return Boolean(res.data.data?.evaluation)
         })
         .catch(err => {
           ElMessage.error("获取智能批改结果异常")
           console.log(err)
           loading.value = false;
+          return false
         })
   }
 
+  const updateCorrectButtonState = () => {
+    showCorrectBtn.value = canCorrectByVersion.value && !hasUnsavedChanges.value
+  }
+
+  const isEvaluationTaskAccepted = (payload: any) => {
+    return payload?.code === 200
+      || payload?.status === 'accepted'
+      || payload?.data?.status === 'accepted'
+  }
+
+  const pollLatestEvaluationResult = (attempt = 0) => {
+    const maxAttempts = 20
+    const intervalMs = 1500
+
+    if (attempt >= maxAttempts) {
+      loading.value = false
+      ElMessage.warning("批改任务已提交,结果仍在生成中,请稍后查看")
+      return
+    }
+
+    apis.getEvaluationVersionList(props.engagement.assignmentId, store.user.id)
+      .then(async (versionRes: any) => {
+        const versionList = versionRes?.data?.data || []
+        if (!versionList.length) {
+          setTimeout(() => pollLatestEvaluationResult(attempt + 1), intervalMs)
+          return
+        }
+
+        const latestVersion = versionList[0].version
+        showCorrectToPageBtn.value = true
+        const hasEvaluation = await getEvaluation(latestVersion)
+        if (hasEvaluation) {
+          return
+        }
+
+        setTimeout(() => pollLatestEvaluationResult(attempt + 1), intervalMs)
+      })
+      .catch((err: any) => {
+        console.error('轮询批改结果失败:', err)
+        setTimeout(() => pollLatestEvaluationResult(attempt + 1), intervalMs)
+      })
+  }
+
   const toEvaluation = () => {
+    if (!manualCorrectRequested.value) {
+      return
+    }
+    manualCorrectRequested.value = false
+
     apis.overAllEvaluation(props.engagement.assignmentId, store.user.id)
         .then(res => {
-          if(res.data.code === 200){
-            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("智能批改请求超时")
+          if (isEvaluationTaskAccepted(res.data)) {
+            console.log('批改任务已提交,开始轮询结果...')
+            pollLatestEvaluationResult()
+          } else {
+            ElMessage.error(res.data?.message || "智能批改请求失败")
             loading.value = false;
           }
         })
@@ -225,8 +264,11 @@ import emitter from "@/utils/emitter";
 
   // 请求批改
   const handleCorrect = () => {
+    if (!showCorrectBtn.value) {
+      return
+    }
+    manualCorrectRequested.value = true
     loading.value = true;
-    showCorrectBtn.value = !showCorrectBtn.value
     toEvaluation()
   };
 
@@ -265,34 +307,109 @@ import emitter from "@/utils/emitter";
   };
 
   /**
-   * 用于判断是否已存在历史批改记录
+   * 优先用 /current 接口判断当前版本是否已有批改结果:
+   * - 有结果 → 直接展示,禁止重复批改
+   * - 无结果 → 再查历史列表,决定按钮状态
    */
   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)
+    if (JSON.stringify(props.engagement) === '{}') return
+
+    const hasContent = props.engagement.textContent && props.engagement.textContent.trim() !== ''
+    if (hasContent) {
+      hasStagedContent.value = true
+    }
+
+    apis.getCurrentVersionEvaluation(props.engagement.assignmentId, store.user.id)
+      .then((currentRes: any) => {
+        const currentEvaluation = currentRes?.data?.data
+        const currentVersion = currentEvaluation?.version
+        const overallStatus = currentEvaluation?.overallStatus ?? 0
+        const sentenceStatus = currentEvaluation?.sentenceStatus ?? 0
+
+        // /current 接口返回的是“状态”,不是“批改内容”
+        // 只有当状态显示已完成时,才认为该版本已批改;批改内容需用 /select/overall 获取
+        if (overallStatus === 1 || sentenceStatus === 1) {
+          canCorrectByVersion.value = false
+          showCorrectToPageBtn.value = true
+
+          // 优先展示整体评价内容(逐句内容在独立页面展示)
+          if (overallStatus === 1 && typeof currentVersion === 'number') {
+            console.info('[EVAL][FE][Silder] current has overall, fetch overall content', {
+              assignmentId: props.engagement.assignmentId,
+              userId: store.user.id,
+              version: currentVersion
+            })
+            apis.getOverall(props.engagement.assignmentId, store.user.id, currentVersion)
+              .then((res: any) => {
+                if (res?.data?.code === 200) {
+                  data.overallEvaluation = res.data.data
+                  console.info('[EVAL][FE][Silder] overall content loaded', {
+                    version: currentVersion,
+                    evalLen: res.data.data?.evaluation?.length ?? -1
+                  })
+                } else {
+                  data.overallEvaluation = null
+                  console.warn('[EVAL][FE][Silder] overall content non-200', {
+                    version: currentVersion,
+                    code: res?.data?.code,
+                    msg: res?.data?.msg
+                  })
+                }
+                updateCorrectButtonState()
+              })
+              .catch((err: any) => {
+                console.error('获取当前版本整体评价失败:', err)
+                data.overallEvaluation = null
+                updateCorrectButtonState()
+              })
+              .finally(() => {
+                // 避免 loading 遮罩导致用户误以为没有内容
+                loading.value = false
+              })
+          } else {
+            data.overallEvaluation = null
+            loading.value = false
+            updateCorrectButtonState()
+          }
+        } else {
+          // 当前版本尚未批改,查询历史列表决定按钮可用性
+          apis.getEvaluationVersionList(props.engagement.assignmentId, store.user.id)
+            .then(res => {
+              const list = res.data.data || []
+              if (list.length === 0) {
+                canCorrectByVersion.value = hasStagedContent.value
+                showCorrectToPageBtn.value = false
+              } else {
+                showCorrectToPageBtn.value = true
+                canCorrectByVersion.value = hasStagedContent.value
+              }
+              updateCorrectButtonState()
+            })
+            .catch(err => {
+              ElMessage.error("获取评价历史记录异常")
+              console.log(err)
+            })
+        }
+      })
+      .catch(err => {
+        console.error('检查当前版本批改状态异常,回退到历史列表查询:', err)
+        apis.getEvaluationVersionList(props.engagement.assignmentId, store.user.id)
           .then(res => {
-            // 从未批改过
-            if(res.data.data.length === 0){
-              // 修改:从未批改过时,只有当已有暂存内容时才能启用批改按钮
-              showCorrectBtn.value = hasStagedContent.value
+            const list = res.data.data || []
+            if (list.length === 0) {
+              canCorrectByVersion.value = hasStagedContent.value
               showCorrectToPageBtn.value = false
             } else {
               showCorrectToPageBtn.value = true
-              showCorrectBtn.value = (res.data.data[0].version !== props.engagement.version) && hasStagedContent.value
+              canCorrectByVersion.value = (list[0].version !== props.engagement.version) && hasStagedContent.value
             }
+            updateCorrectButtonState()
           })
-          .catch(err => {
+          .catch(err2 => {
             ElMessage.error("获取评价历史记录异常")
-            console.log(err)
+            console.log(err2)
           })
-    }
-
+      })
   }
 
   // 添加节流函数
@@ -309,15 +426,16 @@ import emitter from "@/utils/emitter";
 
   // 使用节流包装 handleCorrectTip
   const handleCorrectTip = throttle(() => {
-    if(!showCorrectBtn.value) {
-      ElMessage.info("请更新作文内容并暂存后批改");
+    if (!showCorrectBtn.value) {
+      if (hasUnsavedChanges.value) {
+        ElMessage.info("检测到未保存修改,请先暂存后再批改")
+      } else if (!canCorrectByVersion.value) {
+        ElMessage.info("当前版本已完成智能批改")
+      } else {
+        ElMessage.info("请更新作文内容并暂存后批改")
+      }
     }
-  }, 2000); // 2秒的节流时间
-
-  const handleCorrectToPageTip = throttle(() => {
-    if(!showCorrectToPageBtn.value)
-      ElMessage.info("批改后查看报告")
-  },1000);
+  }, 2000)
 
   // 监听props变化
   watch(() => props.engagement, (newEngagement, oldEngagement) => {
@@ -327,12 +445,21 @@ import emitter from "@/utils/emitter";
   }, { immediate: true });
 
   // 监听暂存成功事件
-  emitter.on('stage-success', () => {
-    console.log('收到暂存成功事件,启用批改按钮')
-    // 暂存成功后,标记已有暂存内容,并启用"开始批改"按钮
+  const handleStageSuccess = () => {
+    console.log('收到暂存成功事件,刷新当前版本批改状态')
     hasStagedContent.value = true
-    showCorrectBtn.value = true
-  })
+    hasUnsavedChanges.value = false
+    // 暂存不会产生新版本:是否可再次批改取决于当前版本是否已批改
+    getEvaluationVersionList()
+  }
+
+  const handleModifiedStateChange = (state: boolean) => {
+    hasUnsavedChanges.value = Boolean(state)
+    updateCorrectButtonState()
+  }
+
+  emitter.on('stage-success', handleStageSuccess)
+  emitter.on('is-modified', handleModifiedStateChange)
 
   onMounted(() => {
     fetchData()
@@ -340,7 +467,8 @@ import emitter from "@/utils/emitter";
 
   onUnmounted(() => {
     // 清理事件监听器
-    emitter.off('stage-success');
+    emitter.off('stage-success', handleStageSuccess);
+    emitter.off('is-modified', handleModifiedStateChange);
   });
 </script>
 

+ 13 - 19
src/views/HomeworkPage/component/HomeworkDetail.vue

@@ -114,15 +114,14 @@ const isAssignmentDisabled = computed(() => {
     return true
   }
 
-  if (now > endTime) {
-    if (engagementStatus.value) {
-      return engagementStatus.value.status !== 'NOT_SUBMITTED'
+  // 普通作业允许多次进入并再次提交;仅考试模式限制已提交/超时后再次进入
+  if (data.examMode === true) {
+    if (engagementStatus.value?.submitted) {
+      return true
+    }
+    if (now > endTime) {
+      return true
     }
-    return false
-  }
-
-  if (data.examMode === true && engagementStatus.value?.submitted) {
-    return true
   }
 
   return false
@@ -140,18 +139,13 @@ const getButtonTooltip = computed(() => {
 
   if (now < startTime) {
     return `作业尚未开始,开始时间:${startTime.toLocaleString()}`
-  } else if (now > endTime) {
-    if (engagementStatus.value) {
-      const status = engagementStatus.value.status
-      if (status === 'NOT_SUBMITTED') {
-        return '作业已截止,但您可以继续完成作业'
-      } else if (status === 'SUBMITTED') {
-        return '作业已截止且已提交,无法再次编辑'
-      } else if (status === 'CORRECTED') {
-        return '作业已截止且已批改,无法再次编辑'
-      }
+  } else if (data.examMode === true && now > endTime) {
+    return '考试已截止,无法再次进入'
+  } else if (!data.examMode && now > endTime) {
+    if (engagementStatus.value?.status === 'NOT_SUBMITTED') {
+      return '作业已截止,但您可以继续完成并补交'
     }
-    return `作业已截止,截止时间:${endTime.toLocaleString()}`
+    return '作业已截止,您可继续修改并再次提交(按补交记录)'
   } else {
     return '点击开始作业'
   }