diff --git a/src/views/components/TrainingAiSidebar.vue b/src/views/components/TrainingAiSidebar.vue
index 73e1c02..430c279 100644
--- a/src/views/components/TrainingAiSidebar.vue
+++ b/src/views/components/TrainingAiSidebar.vue
@@ -38,7 +38,8 @@
- AI智能评分
+
请先至少填写并保存一项实训内容,再请求 AI 助评。
+ AI智能评分
@@ -84,6 +85,7 @@ const props = defineProps({
default: () => [],
},
taskKey: { type: String, default: "" },
+ hasSavedAnswer: { type: Boolean, default: true },
});
const emit = defineEmits(["request-assist", "request-evaluation"]);
@@ -135,7 +137,10 @@ async function requestAssist() {
async function requestEvaluation() {
emit("request-evaluation", reviewQuestion.value);
- if (!currentTaskKey.value) return;
+ if (!currentTaskKey.value || !props.hasSavedAnswer) {
+ internalEvalMessage.value = "请先至少填写并保存一项实训内容,再请求 AI 助评。";
+ return;
+ }
assessmentLoading.value = true;
try {
await requestAiTrainingAssessment(currentTaskKey.value);
@@ -143,6 +148,8 @@ async function requestEvaluation() {
internalScoreVisible.value = reports.value.score !== null;
internalScore.value = reports.value.score ?? "--";
internalEvalMessage.value = reports.value.assessmentReport || props.evalMessage;
+ } catch (error) {
+ internalEvalMessage.value = "AI 助评暂时不可用,请稍后重试。";
} finally { assessmentLoading.value = false; }
}
@@ -228,6 +235,7 @@ async function requestEvaluation() {
.reference-block { width: 100%; border: 0; background: transparent; padding: 14px 0 0; text-align: left; cursor: pointer; }
.reference-block:hover strong { color: #77eaff; }
.assessment-button { width: 100%; margin-top: 14px; min-height: 44px; font-weight: 800; }
+.assessment-hint { margin: 12px 0 -2px; color: #9fcbe2; font-size: 13px; line-height: 1.6; }
.output-glow {
position: absolute;
diff --git a/src/views/training/GenericTrainingPage.vue b/src/views/training/GenericTrainingPage.vue
index 701d259..ca66d2e 100644
--- a/src/views/training/GenericTrainingPage.vue
+++ b/src/views/training/GenericTrainingPage.vue
@@ -157,6 +157,7 @@
@@ -201,6 +202,7 @@ const buildInitialProcessRows = () =>
const form = ref(buildInitialForm());
const workRows = ref(buildInitialRows());
const processRows = ref(buildInitialProcessRows());
+const hasSavedAnswer = ref(false);
const hasAnyAnswer = computed(() =>
Object.values(form.value).some(hasMeaningfulAnswer) ||
workRows.value.some((row) => hasMeaningfulAnswer(row.answer)) ||
@@ -225,6 +227,7 @@ watch(
form.value = buildInitialForm();
workRows.value = buildInitialRows();
processRows.value = buildInitialProcessRows();
+ hasSavedAnswer.value = false;
await loadSavedAnswer();
},
{ immediate: true }
@@ -352,6 +355,9 @@ async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(pageKey.value);
const answer = res?.data;
+ hasSavedAnswer.value = Boolean(
+ answer && [answer.step1Answer, answer.step2Answer, answer.step3Answer, answer.step4Answer, answer.dynamicStepAnswers].some(hasMeaningfulAnswer)
+ );
if (answer?.currentStep && Number(answer.currentStep) > 0) {
currentStep.value = Number(answer.currentStep);
}
@@ -377,6 +383,7 @@ async function saveAnswer(saveAction = "SAVE") {
saving.value = true;
try {
await saveStudentTrainingAnswer(pageKey.value, buildAnswerPayload(saveAction));
+ hasSavedAnswer.value = hasAnyAnswer.value;
proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "已保存");
} catch (error) {
proxy?.$modal?.msgError?.(saveAction === "SUBMIT" ? "提交失败" : "保存失败");
diff --git a/tests/ai-training-evaluation.static.test.cjs b/tests/ai-training-evaluation.static.test.cjs
index 131d196..799392c 100644
--- a/tests/ai-training-evaluation.static.test.cjs
+++ b/tests/ai-training-evaluation.static.test.cjs
@@ -5,14 +5,14 @@ const path = require("path");
const root = path.resolve(__dirname, "..");
const api = fs.readFileSync(path.join(root, "src/api/aiTrainingEvaluation.js"), "utf8");
const component = fs.readFileSync(path.join(root, "src/views/components/TrainingAiEvaluation.vue"), "utf8");
+const sidebar = fs.readFileSync(path.join(root, "src/views/components/TrainingAiSidebar.vue"), "utf8");
const page = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8");
const { normalizeEvaluationReports } = require(path.join(root, "src/views/components/trainingAiEvaluationReport.cjs"));
assert(/`\/api\/student\/training-tasks\/\$\{taskKey\}\/ai-evaluation`/.test(api), "evaluation API should use the student task endpoint");
assert(/\/help/.test(api) && /\/assessment/.test(api), "evaluation API should expose help and assessment operations");
-assert(/TrainingAiEvaluation/.test(page) && /:has-any-answer="hasAnyAnswer"/.test(page), "generic training page should pass meaningful answer state to AI evaluation");
+assert(/TrainingAiSidebar/.test(page) && /:has-saved-answer="hasSavedAnswer"/.test(page), "generic training page should pass persisted answer state to the AI sidebar");
assert(/form\.value/.test(page) && /workRows\.value/.test(page) && /processRows\.value/.test(page), "hasAnyAnswer should inspect all supported answer shapes");
-assert(/\s*props\.taskKey/.test(component), "AI evaluation should load on mount and task changes");
assert(/AI助学/.test(component) && /AI助评/.test(component), "AI evaluation should offer separate help and assessment controls");
@@ -56,3 +56,7 @@ assert.strictEqual(malformedPayload.assessmentReport, "{also not valid JSON", "m
assert.strictEqual(malformedPayload.score, null, "malformed assessment JSON should not produce a score");
assert.deepStrictEqual(malformedPayload.scoreCriteria, [], "malformed assessment JSON should not produce criteria");
assert(/retry/.test(component) || /重试/.test(component), "failed AI operations should expose a retry action");
+assert(/hasSavedAnswer/.test(page) && /:has-saved-answer="hasSavedAnswer"/.test(page), "generic training page should distinguish persisted answers before enabling AI assessment");
+assert(/hasSavedAnswer/.test(sidebar), "AI sidebar should accept persisted-answer state");
+assert(/!props\.hasSavedAnswer/.test(sidebar), "AI assessment should be disabled until a saved answer exists");
+assert(/catch \(error\)/.test(sidebar), "AI sidebar should handle assessment failures without an unhandled rejection");