Kaynağa Gözat

fix:修复写作页面显示

lalala 4 ay önce
ebeveyn
işleme
f50fb92f53

+ 19 - 2
src/store/index.ts

@@ -8,7 +8,11 @@ export const useStore = defineStore('user', {
             id: null,
             name: ""
         } as IUser,
-        token: ''
+        token: '',
+        // 智能批改进行中:用于前端统一禁用提交/暂存/历史查看等操作
+        aiEvalBusy: false,
+        // 解决“刚发起批改但 /current 还未更新为处理中(2)”的短窗口:强制锁定到该时间点(ms)
+        aiEvalBusyLockUntil: 0
     }),
     actions: {
         setToken(token: string) {
@@ -17,9 +21,22 @@ export const useStore = defineStore('user', {
         setUser(user: IUser) {
             this.user = user;
         },
+        setAiEvalBusy(busy: boolean) {
+            this.aiEvalBusy = busy;
+        },
+        lockAiEvalBusy(ms: number) {
+            const until = Date.now() + Math.max(0, ms);
+            this.aiEvalBusy = true;
+            this.aiEvalBusyLockUntil = Math.max(this.aiEvalBusyLockUntil || 0, until);
+        },
+        clearAiEvalBusyLock() {
+            this.aiEvalBusyLockUntil = 0;
+        },
         clear(){
             this.user = null;
             this.token = ''
+            this.aiEvalBusy = false
+            this.aiEvalBusyLockUntil = 0
         }
     },
     // 持久化,pinia-plugin-persistedstate
@@ -27,7 +44,7 @@ export const useStore = defineStore('user', {
         // enabled: true, 
         key: 'user-storage',  
         storage: sessionStorage,  
-        paths: ['token', 'user']  
+        paths: ['token', 'user', 'aiEvalBusy', 'aiEvalBusyLockUntil']  
             
     },
 });

+ 72 - 10
src/views/AIEvaluationPage/AIEvaluationPage.vue

@@ -135,11 +135,11 @@
         <!-- 版本列表 -->
         <el-scrollbar height="800px" class="version-scrollbar">
           <div
-              v-for="version in data.versionList"
-              :key="version.id"
-              class="version-item"
-              :class="{ active: selectedVersion === version.version }"
-              @click="getEvaluation(version.version)"
+            v-for="version in data.versionList"
+            :key="version.id"
+            class="version-item"
+            :class="{ active: selectedVersion === version.version, disabled: aiEvalBusy }"
+            @click="!aiEvalBusy && getEvaluation(version.version)"
           >
             <div class="version-marker"></div>
             <div class="version-info">
@@ -192,8 +192,12 @@
   })
   const selectedVersion = ref<number>(1)
   const currentVersion = ref<number | null>(null)
+  const currentOverallStatus = ref<number>(0)
+  const currentSentenceStatus = ref<number>(0)
   const isLoadingSentence = ref(false) // 逐句评价加载状态
   const isLoadingOverall = ref(false) // 整体评价加载状态
+  const aiEvalBusy = ref(false) // 后端正在批改:禁止查看历史/切换版本等
+  let aiEvalBusyPolling: NodeJS.Timeout | null = null
   let sentenceLoadingTimer: NodeJS.Timeout | null = null // 逐句评价延迟加载计时器
   let sentencePollingInterval: NodeJS.Timeout | null = null // 逐句评价轮询计时器
 
@@ -392,13 +396,14 @@
           // 从未批改过
           if(res.data.data.length === 0){
             data.versionList = []
-            if (currentVersion.value != null) {
-              getEvaluation(currentVersion.value)
-            }
+            // 没有任何历史记录时,不主动请求 /select/overall,避免进入页面就报“AI批改不存在”
+            data.overallEvaluation = null
+            data.sentenceEvaluationList = []
           } else {
             data.versionList = res.data.data
-            // 优先展示“当前最新提交版本”,否则回落到列表第一项(最新批改版本)
-            const prefer = currentVersion.value ?? res.data.data[0].version
+            // 仅当“当前版本已批改完成”才优先展示当前版本;否则直接展示历史第一条(最新批改记录)
+            const currentDone = (currentOverallStatus.value === 1) || (currentSentenceStatus.value === 1)
+            const prefer = (currentDone && currentVersion.value != null) ? currentVersion.value : res.data.data[0].version
             getEvaluation(prefer)
           }
         })
@@ -414,12 +419,48 @@
         currentVersion.value = res.data.data.version
         selectedVersion.value = res.data.data.version
       }
+      const overallStatus = res.data.data?.overallStatus ?? 0
+      const sentenceStatus = res.data.data?.sentenceStatus ?? 0
+      currentOverallStatus.value = overallStatus
+      currentSentenceStatus.value = sentenceStatus
+      const backendBusy = overallStatus === 2 || sentenceStatus === 2
+      const locked = Boolean(store.aiEvalBusyLockUntil && Date.now() < store.aiEvalBusyLockUntil)
+      aiEvalBusy.value = backendBusy || locked
+      store.setAiEvalBusy(aiEvalBusy.value)
     } catch (e) {
       // 后端未部署 current 接口/用户未参与等情况,继续走历史列表逻辑
       console.warn('获取当前版本失败:', e)
     }
   }
 
+  const stopAiEvalBusyPolling = () => {
+    if (aiEvalBusyPolling) {
+      clearInterval(aiEvalBusyPolling)
+      aiEvalBusyPolling = null
+    }
+  }
+
+  const startAiEvalBusyPolling = () => {
+    if (aiEvalBusyPolling) return
+    aiEvalBusyPolling = setInterval(async () => {
+      try {
+        const res = await apis.getCurrentVersionEvaluation(assignmentId, store.user.id)
+        if (res.data.code !== 200) return
+        const overallStatus = res.data.data?.overallStatus ?? 0
+        const sentenceStatus = res.data.data?.sentenceStatus ?? 0
+        const busy = overallStatus === 2 || sentenceStatus === 2
+        aiEvalBusy.value = busy
+        if (!busy) {
+          stopAiEvalBusyPolling()
+          // 批改结束后刷新版本列表,保证能看到最新版本
+          getEvaluationVersionList()
+        }
+      } catch {
+        // ignore
+      }
+    }, 1500)
+  }
+
   const getBackgroundColor = (sentenceEvaluation: SentenceEvaluation) => {
     if (sentenceEvaluation.id === highlightSentenceId.value) {
       switch (sentenceEvaluation.type) {
@@ -457,9 +498,23 @@
     // getEvaluation()
     fetchCurrentVersion().finally(() => {
       getEvaluationVersionList()
+      if (aiEvalBusy.value) {
+        startAiEvalBusyPolling()
+      }
     })
   })
 
+  // 若用户从写作页跳转过来,先用 store 的 busy 立即禁用交互,再由后端状态纠正
+  watch(
+    () => store.aiEvalBusy,
+    (v) => {
+      if (aiEvalBusy.value !== Boolean(v)) {
+        aiEvalBusy.value = Boolean(v)
+      }
+    },
+    { immediate: true }
+  )
+
   // 组件卸载时清理定时器
   onUnmounted(() => {
     if (sentenceLoadingTimer) {
@@ -470,6 +525,7 @@
       clearInterval(sentencePollingInterval)
       sentencePollingInterval = null
     }
+    stopAiEvalBusyPolling()
   })
 </script>
 
@@ -936,6 +992,12 @@ export default {
   background: #87CEEB;
 }
 
+.version-item.disabled {
+  opacity: 0.55;
+  cursor: not-allowed;
+  pointer-events: auto;
+}
+
 .version-info {
   flex: 1;
   min-width: 0;

+ 45 - 3
src/views/Home/component/Edit/index.vue

@@ -11,8 +11,24 @@
         <QuillEditor/>
       </div>
       <div class="button-right">
-        <el-button type="primary" @click="handleSubmit"  color="#4A90C4" style="color: #FFFFFF; width: 70px;">提交</el-button>
-        <el-button type="primary" @click="handleStage"  color="#4A90C4" style="color: #FFFFFF; width: 70px; margin-left: 15px;">暂存</el-button>
+        <el-button
+          type="primary"
+          @click="handleSubmit"
+          :disabled="aiEvalBusy || store.aiEvalBusy"
+          color="#4A90C4"
+          style="color: #FFFFFF; width: 70px;"
+        >
+          提交
+        </el-button>
+        <el-button
+          type="primary"
+          @click="handleStage"
+          :disabled="aiEvalBusy || store.aiEvalBusy"
+          color="#4A90C4"
+          style="color: #FFFFFF; width: 70px; margin-left: 15px;"
+        >
+          暂存
+        </el-button>
       </div>
     </div>
   </div>
@@ -22,7 +38,7 @@
 
   import "./index.scss";
   import type {engagementVO, IAssignment} from "@/types/vo";
-  import {defineProps, onMounted, reactive, ref} from 'vue'
+  import {defineProps, onMounted, onUnmounted, reactive, ref} from 'vue'
   import router from '@/router'
   import useApis from "@/apis";
   import {useStore} from "@/store";
@@ -41,6 +57,7 @@
   const store = useStore()
 
   let hasStage = ref(true)
+  const aiEvalBusy = ref(false)
   let data = reactive<{
     assignment: IAssignment
   }>({
@@ -67,6 +84,17 @@
     Object.assign(getQuillContent, value)
   })
 
+  const onAiEvalBusy = (busy: any) => {
+    const v = Boolean(busy)
+    aiEvalBusy.value = v
+    store.setAiEvalBusy(v)
+    if (aiEvalBusy.value) {
+      ElMessage.warning('智能批改进行中:已暂时禁用提交/暂存/查看历史等操作')
+    }
+  }
+
+  emitter.on('ai-eval-busy', onAiEvalBusy)
+
   // 成功操作的消息提示
   const success = (msg: string) => {
     ElMessage.success(msg);
@@ -80,6 +108,10 @@
 
   // 提交操作
   const handleSubmit = async () => {
+    if (aiEvalBusy.value || store.aiEvalBusy) {
+      ElMessage.warning('智能批改进行中,请稍候再提交')
+      return
+    }
     if (props.isExamMode) {
       try {
         await ElMessageBox.confirm(
@@ -125,6 +157,10 @@
 
   // 暂存操作
   const handleStage = async () => {
+    if (aiEvalBusy.value || store.aiEvalBusy) {
+      ElMessage.warning('智能批改进行中,请稍候再暂存')
+      return
+    }
     try {
       console.log(props)
       const _res = await apis.engagementStage(store.user.id, props.engagement.assignmentId, JSON.stringify(getQuillContent), getText.value, getHTML.value);
@@ -170,8 +206,14 @@
 
   onMounted(() => {
     fetchData()
+    // 页面刷新/组件后挂载时,兜底从 store 恢复 busy
+    aiEvalBusy.value = Boolean(store.aiEvalBusy)
   });
 
+  onUnmounted(() => {
+    emitter.off('ai-eval-busy', onAiEvalBusy)
+  })
+
 
 </script>
 

+ 145 - 6
src/views/Home/component/Silder/index.vue

@@ -54,12 +54,28 @@
             </template>
           </div>
         </template>
-        <div>智能批改</div> 
+        <div style="display: flex; align-items: center; gap: 8px;">
+          <div>智能批改</div>
+          <el-tag
+            size="small"
+            effect="light"
+            style="margin-left: 6px; border: 1px solid #52A779; color: #52A779; background-color: #ffffff; font-weight: 600;"
+          >
+            v{{ props.engagement?.version ?? '-' }}
+          </el-tag>
+        </div>
         <div class="edit-box-header">
-          <el-button @click="handleCorrect" :disabled="!showCorrectBtn" @mouseover="handleCorrectTip">开始批改</el-button>
+          <el-button
+            @click="handleCorrect"
+            :disabled="!showCorrectBtn || aiEvalBusy || store.aiEvalBusy"
+            @mouseover="handleCorrectTip"
+          >
+            开始批改
+          </el-button>
           <el-button
             v-if="showCorrectToPageBtn"
             @click="handleCorrectToPage"
+            :disabled="aiEvalBusy || store.aiEvalBusy"
           >
             查看过往批改记录
           </el-button>
@@ -70,7 +86,10 @@
         <div class="capacity-item">
           <div v-loading="loading" element-loading-text="智能批改中" class="remark">
             <div v-if="data.overallEvaluation?.evaluation">
-              {{ data.overallEvaluation.evaluation }}
+              <div
+                class="markdown-content"
+                v-html="renderMarkdown(data.overallEvaluation.evaluation)"
+              />
             </div>
             <div v-else class="remark-empty">
               <el-empty
@@ -87,7 +106,7 @@
 </template>
 
 <script lang="ts" setup>
-import {defineProps, nextTick, onMounted, onUnmounted, reactive, ref, watch, watchEffect} from 'vue'
+import {defineProps, onMounted, onUnmounted, reactive, ref, watch} from 'vue'
   import "./index.scss"
 import type {engagementVO, IAssignment} from "@/types/vo";
   import useApis from "@/apis";
@@ -99,6 +118,8 @@ import {useRoute} from "vue-router";
 import {ElMessage} from "element-plus";
 import router from "@/router";
 import emitter from "@/utils/emitter";
+import { marked } from "marked";
+import * as DOMPurifyModule from "dompurify";
 
 
   interface IPreviewProps {
@@ -114,7 +135,6 @@ import emitter from "@/utils/emitter";
   }>()
 
   let isShrink = ref(false)
-  let aiRemark = ref("")
   let loading = ref(false)
   let open= ref(false)
   let showCorrectBtn = ref(false)
@@ -123,6 +143,8 @@ import emitter from "@/utils/emitter";
   let hasStagedContent = ref(false) // 用于跟踪是否已暂存内容
   let hasUnsavedChanges = ref(false) // 用于跟踪是否有未保存改动
   let manualCorrectRequested = ref(false) // 仅允许手动点击后触发批改
+  const aiEvalBusy = ref(false)
+  let aiEvalBusyPolling: NodeJS.Timeout | null = null
 
   // 获得路由中的assignmentId
   const route = useRoute()
@@ -196,6 +218,83 @@ import emitter from "@/utils/emitter";
       || payload?.data?.status === 'accepted'
   }
 
+  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 setAiEvalBusy = (busy: boolean) => {
+    if (aiEvalBusy.value === busy) return
+    aiEvalBusy.value = busy
+    store.setAiEvalBusy(busy)
+    emitter.emit('ai-eval-busy', busy)
+  }
+
+  const isWithinBusyLockWindow = () => {
+    return Boolean(store.aiEvalBusyLockUntil && Date.now() < store.aiEvalBusyLockUntil)
+  }
+
+  const stopAiEvalBusyPolling = () => {
+    if (aiEvalBusyPolling) {
+      clearInterval(aiEvalBusyPolling)
+      aiEvalBusyPolling = null
+    }
+  }
+
+  const startAiEvalBusyPolling = () => {
+    if (aiEvalBusyPolling) return
+    aiEvalBusyPolling = setInterval(async () => {
+      try {
+        const res = await apis.getCurrentVersionEvaluation(props.engagement.assignmentId, store.user.id)
+        const cur = res?.data?.data
+        const overallStatus = cur?.overallStatus ?? 0
+        const sentenceStatus = cur?.sentenceStatus ?? 0
+        const busy = overallStatus === 2 || sentenceStatus === 2
+        // 若刚发起批改请求但后端 /current 还未切到处理中(2),保持 busy(防止用户提前点历史/提交)
+        if (busy) {
+          setAiEvalBusy(true)
+        } else if (isWithinBusyLockWindow()) {
+          setAiEvalBusy(true)
+        } else {
+          setAiEvalBusy(false)
+          store.clearAiEvalBusyLock()
+        }
+        if (!busy) {
+          stopAiEvalBusyPolling()
+        }
+      } catch {
+        // ignore
+      }
+    }, 1500)
+  }
+
   const pollLatestEvaluationResult = (attempt = 0) => {
     const maxAttempts = 20
     const intervalMs = 1500
@@ -218,6 +317,7 @@ import emitter from "@/utils/emitter";
         showCorrectToPageBtn.value = true
         const hasEvaluation = await getEvaluation(latestVersion)
         if (hasEvaluation) {
+          setAiEvalBusy(false)
           return
         }
 
@@ -229,6 +329,8 @@ import emitter from "@/utils/emitter";
       })
   }
 
+  const didReloadAfterEvalAccepted = ref(false)
+
   const toEvaluation = () => {
     if (!manualCorrectRequested.value) {
       return
@@ -239,16 +341,28 @@ import emitter from "@/utils/emitter";
         .then(res => {
           if (isEvaluationTaskAccepted(res.data)) {
             console.log('批改任务已提交,开始轮询结果...')
+            setAiEvalBusy(true)
+            startAiEvalBusyPolling()
             pollLatestEvaluationResult()
+
+            // 后端已受理批改任务后,立即刷新页面,避免用户在请求过程中误操作
+            if (!didReloadAfterEvalAccepted.value) {
+              didReloadAfterEvalAccepted.value = true
+              setTimeout(() => {
+                window.location.reload()
+              }, 200)
+            }
           } else {
             ElMessage.error(res.data?.message || "智能批改请求失败")
             loading.value = false;
+            setAiEvalBusy(false)
           }
         })
         .catch(err => {
           ElMessage.error("智能批改请求异常")
           console.log(err)
           loading.value = false;
+          setAiEvalBusy(false)
         })
     apis.sentenceEvaluation(props.engagement.assignmentId, store.user.id)
         .then(res => {
@@ -269,10 +383,18 @@ import emitter from "@/utils/emitter";
     }
     manualCorrectRequested.value = true
     loading.value = true;
+    // 先本地强制锁定一段时间,避免 /current 还没变成 2 的窗口期
+    store.lockAiEvalBusy(15000)
+    setAiEvalBusy(true)
+    startAiEvalBusyPolling()
     toEvaluation()
   };
 
   const handleCorrectToPage = () => {
+    if (aiEvalBusy.value || store.aiEvalBusy) {
+      ElMessage.warning('智能批改进行中,暂不可查看历史记录')
+      return
+    }
     router.push({
       path: `/home/assignment/${props.engagement.assignmentId}/AIEvaluation`,
     }).then(() => {
@@ -463,8 +585,25 @@ import emitter from "@/utils/emitter";
 
   onMounted(() => {
     fetchData()
+    // 若进入页面时后端显示正在批改,则同步 busy 状态并开启轮询
+    apis.getCurrentVersionEvaluation(props.engagement.assignmentId, store.user.id)
+      .then((res: any) => {
+        const cur = res?.data?.data
+        const overallStatus = cur?.overallStatus ?? 0
+        const sentenceStatus = cur?.sentenceStatus ?? 0
+        const busy = overallStatus === 2 || sentenceStatus === 2
+        if (busy) {
+          setAiEvalBusy(true)
+          startAiEvalBusyPolling()
+        }
+      })
+      .catch(() => {})
   });
 
+  onUnmounted(() => {
+    stopAiEvalBusyPolling()
+  })
+
   onUnmounted(() => {
     // 清理事件监听器
     emitter.off('stage-success', handleStageSuccess);
@@ -474,6 +613,6 @@ import emitter from "@/utils/emitter";
 
 <script lang="ts">
 export default {
-  name: "Slider"
+  name: "WritingSlider"
 }
 </script>