|
|
@@ -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>
|