|
|
@@ -64,7 +64,14 @@
|
|
|
<div class="rectangle">
|
|
|
<template v-if="selected === 'trapezoid'">
|
|
|
<el-scrollbar height="100%" class="evaluation-scrollbar">
|
|
|
- <div class="overall-evaluation-content">
|
|
|
+ <!-- 整体评价加载状态 -->
|
|
|
+ <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" >
|
|
|
@@ -81,11 +88,29 @@
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
<el-scrollbar height="100%" class="evaluation-scrollbar">
|
|
|
- <div class="sentence-evaluation-content">
|
|
|
+ <!-- 逐句评价加载状态 -->
|
|
|
+ <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>
|
|
|
+ <template v-else-if="data.sentenceEvaluationList?.length > 0 && !data.sentenceEvaluationList[0].evaluation">
|
|
|
+ <div class="no-evaluation">
|
|
|
+ <div style="margin-bottom: 10px;">⚠️ 后端返回的逐句评价数据不完整</div>
|
|
|
+ <div style="font-size: 12px; color: #666;">
|
|
|
+ 已收到 {{ data.sentenceEvaluationList.length }} 条评价记录,但缺少评价内容(evaluation)和分类(category)字段。<br/>
|
|
|
+ 请联系后端检查 AI 评价生成逻辑。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
<el-card v-for="sentenceEvaluation in data.sentenceEvaluationList"
|
|
|
+ v-else
|
|
|
:key="sentenceEvaluation.id"
|
|
|
class="sentence-card"
|
|
|
shadow="hover"
|
|
|
@@ -141,13 +166,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,
|
|
|
@@ -171,6 +196,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)
|
|
|
@@ -197,29 +226,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)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -276,6 +387,18 @@
|
|
|
// getEvaluation()
|
|
|
getEvaluationVersionList()
|
|
|
})
|
|
|
+
|
|
|
+ // 组件卸载时清理定时器
|
|
|
+ onUnmounted(() => {
|
|
|
+ if (sentenceLoadingTimer) {
|
|
|
+ clearTimeout(sentenceLoadingTimer)
|
|
|
+ sentenceLoadingTimer = null
|
|
|
+ }
|
|
|
+ if (sentencePollingInterval) {
|
|
|
+ clearInterval(sentencePollingInterval)
|
|
|
+ sentencePollingInterval = null
|
|
|
+ }
|
|
|
+ })
|
|
|
</script>
|
|
|
|
|
|
<script lang="ts">
|
|
|
@@ -288,9 +411,10 @@ export default {
|
|
|
/* 主容器 - 响应式布局 */
|
|
|
.ai-evaluation-container {
|
|
|
display: flex;
|
|
|
+ align-items: stretch; /* 让所有子元素高度一致 */
|
|
|
gap: 10px;
|
|
|
padding: 10px;
|
|
|
- min-height: 90vh;
|
|
|
+ height: calc(100vh - 80px); /* 使用视口高度减去顶部导航栏等空间 */
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
@@ -340,7 +464,7 @@ export default {
|
|
|
white-space: pre-wrap;
|
|
|
margin-top: 40px;
|
|
|
line-height: 30px;
|
|
|
- height: calc(90vh - 80px);
|
|
|
+ flex: 1; /* 自动填充剩余空间 */
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
@@ -360,7 +484,6 @@ export default {
|
|
|
.evaluation-section {
|
|
|
flex: 0 0 40%;
|
|
|
min-width: 300px;
|
|
|
- height: 89vh;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
@@ -491,6 +614,32 @@ export default {
|
|
|
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;
|
|
|
@@ -531,7 +680,6 @@ export default {
|
|
|
.version-section {
|
|
|
flex: 0 0 20%;
|
|
|
min-width: 180px;
|
|
|
- height: 90vh;
|
|
|
border-left: 1px solid #e4e7ed;
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
box-shadow: -2px 0 4px rgba(0,0,0,0.05);
|
|
|
@@ -634,17 +782,24 @@ export default {
|
|
|
@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: auto;
|
|
|
+ height: 40vh;
|
|
|
min-height: 300px;
|
|
|
border-left: none;
|
|
|
border-top: 1px solid #e4e7ed;
|
|
|
@@ -663,6 +818,7 @@ export default {
|
|
|
flex-direction: column;
|
|
|
padding: 5px;
|
|
|
gap: 8px;
|
|
|
+ height: auto; /* 移动端垂直排列,高度自适应 */
|
|
|
}
|
|
|
|
|
|
.essay-section,
|
|
|
@@ -674,19 +830,19 @@ export default {
|
|
|
}
|
|
|
|
|
|
.essay-section {
|
|
|
- height: 50vh;
|
|
|
+ height: 45vh;
|
|
|
}
|
|
|
|
|
|
.essay-content-wrapper {
|
|
|
- height: calc(50vh - 80px);
|
|
|
+ height: calc(100% - 80px);
|
|
|
}
|
|
|
|
|
|
.evaluation-section {
|
|
|
- height: 60vh;
|
|
|
+ height: 45vh;
|
|
|
}
|
|
|
|
|
|
.version-section {
|
|
|
- height: 40vh;
|
|
|
+ height: 35vh;
|
|
|
}
|
|
|
|
|
|
.trapezoid,
|