From ae64fb346596088b3ba65cdf856a37906fec9544 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Wed, 14 Oct 2026 14:37:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20BI=E5=8F=AF=E8=A7=86=E5=8C=96=E5=88=86?= =?UTF-8?q?=E6=9E=90=E6=94=B9=E8=B5=B0/bi/=E5=90=8C=E6=9C=BA=E5=8F=8D?= =?UTF-8?q?=E4=BB=A3=E4=BF=AE=E5=A4=8D=E8=B7=A8=E5=9F=9F=E4=B8=A2Cookie?= =?UTF-8?q?=E5=BC=B9=E7=99=BB=E5=BD=95=EF=BC=9BAI=E5=8A=A9=E5=AD=A6/?= =?UTF-8?q?=E5=8A=A9=E8=AF=84=E5=89=8D=E7=BD=AE=E6=A0=A1=E9=AA=8C=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E4=B8=AD=E6=96=87=E5=8C=96=EF=BC=9B=E6=95=99=E5=B8=88?= =?UTF-8?q?=E7=AB=AF=E5=AE=9E=E8=AE=AD=E7=AE=A1=E7=90=86=E6=96=87=E6=A1=88?= =?UTF-8?q?=E4=B8=8E=E6=90=9C=E7=B4=A2=E7=BF=BB=E9=A1=B5=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.production | 5 +- src/api/AI.js | 8 ++ src/api/aiTrainingEvaluation.js | 16 +++ src/api/index.js | 3 +- src/api/projectStudyTime.js | 1 + src/views/analysis-of-students/dialogue.vue | 111 ++++++++++++------ src/views/components/TrainingAiEvaluation.vue | 6 +- src/views/components/TrainingAiSidebar.vue | 5 +- src/views/schoolAdmin/admin-theme.scss | 41 +++++-- src/views/schoolAdmin/class/index.vue | 7 +- src/views/schoolAdmin/faculty/index.vue | 5 - src/views/schoolAdmin/major/index.vue | 5 - src/views/schoolAdmin/school/index.vue | 5 - src/views/schoolAdmin/student/index.vue | 102 ++++------------ src/views/schoolAdmin/teacher/index.vue | 45 +------ src/views/teacherEnd/class/index.vue | 8 +- src/views/teacherEnd/major/index.vue | 7 +- src/views/teacherEnd/school/index.vue | 7 +- src/views/teacherEnd/student/index.vue | 106 ++++------------- src/views/teacherEnd/teacher/index.vue | 22 ++-- src/views/teacherEnd/trainingTask/index.vue | 31 ++--- tests/teacher-import-template.static.test.cjs | 6 +- ...eacher-student-roster-mode.static.test.cjs | 4 +- 23 files changed, 240 insertions(+), 316 deletions(-) diff --git a/.env.production b/.env.production index d74b828..dc0acff 100644 --- a/.env.production +++ b/.env.production @@ -15,6 +15,9 @@ VITE_APP_ENV = 'production' # 若依管理系统/生产环境 VITE_APP_BASE_API = '/' VITE_APP_AI_API = 'https://whmnjy.sztzjy.com:8764/' -VITE_APP_BI_URL = 'https://dshlw.sztzjy.com:196' +# BI 可视化分析:走本机 Nginx 的 /bi/ 同机反代,保持与主系统同源, +# 登录 Cookie(Admin-Token)才能随 iframe 请求带给 BI,避免 BI 弹自己的登录页。 +# 若部署机可直接用 *.sztzjy.com 域名 + HTTPS 访问,也可改回绝对地址 https://dshlw.sztzjy.com:196 +VITE_APP_BI_URL = '/bi/' # 是否在打包时开启压缩,支持 gzip 和 brotli VITE_BUILD_COMPRESS = gzip diff --git a/src/api/AI.js b/src/api/AI.js index 24895f9..2f74787 100644 --- a/src/api/AI.js +++ b/src/api/AI.js @@ -225,5 +225,13 @@ export function getLearningAnalysis(params) { data: stuData, // AI 服务使用独立鉴权,不能携带本系统的登录令牌。 headers: { isToken: false } + }).then((res) => { + // AI 服务端点故障时返回 HTTP 200 + "Error: ..." 纯文本,这里统一转成失败, + // 由调用方回退到可用的 AI 对话通道(api/kimi4)。 + const text = typeof res === 'string' ? res.trim() : String(res?.data ?? '').trim() + if (/^error\b/i.test(text)) { + throw new Error(text) + } + return res }) } diff --git a/src/api/aiTrainingEvaluation.js b/src/api/aiTrainingEvaluation.js index f935116..90b4690 100644 --- a/src/api/aiTrainingEvaluation.js +++ b/src/api/aiTrainingEvaluation.js @@ -1,5 +1,17 @@ import request from "@/utils/request"; +/** + * 后端 AI 助学/助评的前置校验消息是英文(该文案要落库,不能直接改中文), + * 前端按场景转成中文引导;未识别的消息回退到调用方给定的兜底文案。 + */ +export function friendlyAiEvaluationMessage(error, fallback) { + const raw = String(error?.response?.data?.msg || error?.message || ""); + if (raw.includes("at least one training step")) { + return "请先在实训页填写并保存至少一个步骤的内容,再使用 AI 助学/助评。"; + } + return fallback; +} + export function getAiTrainingEvaluation(taskKey) { return request({ url: `/api/student/training-tasks/${taskKey}/ai-evaluation`, @@ -13,6 +25,9 @@ export function requestAiTrainingHelp(taskKey, question, stepContext = "") { method: "post", data: { question, stepContext }, headers: { repeatSubmit: false }, + // 后端对这类前置校验返回英文原文(还要落库,不能直接改中文), + // 屏蔽全局错误弹窗,由侧边栏按场景给出中文提示 + skipErrorMessage: true, }); } @@ -21,5 +36,6 @@ export function requestAiTrainingAssessment(taskKey) { url: `/api/student/training-tasks/${taskKey}/ai-evaluation/assessment`, method: "post", headers: { repeatSubmit: false }, + skipErrorMessage: true, }); } diff --git a/src/api/index.js b/src/api/index.js index 7b32744..876f453 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -57,7 +57,8 @@ export const getUserRank = (params) => { }) } export const getCurrentExperimentReport = () => { - return request({ url: '/api/student/experiment-report/current', method: 'get' }) + // 未加入教学班等场景后端返回 400,页面有自己的空态/错误提示,不在拦截器里重复弹原始报错 + return request({ url: '/api/student/experiment-report/current', method: 'get', skipErrorMessage: true }) } // 更新访问次数 export const updateVisitCount = (params) => { diff --git a/src/api/projectStudyTime.js b/src/api/projectStudyTime.js index 35bbfca..9f95c08 100644 --- a/src/api/projectStudyTime.js +++ b/src/api/projectStudyTime.js @@ -13,5 +13,6 @@ export function getCurrentProjectStudyTime() { return request({ url: "/api/student/project-study-time/current", method: "get", + skipErrorMessage: true, }); } diff --git a/src/views/analysis-of-students/dialogue.vue b/src/views/analysis-of-students/dialogue.vue index 74aff6b..e682e81 100644 --- a/src/views/analysis-of-students/dialogue.vue +++ b/src/views/analysis-of-students/dialogue.vue @@ -94,31 +94,17 @@ const messages = ref([]); const goBack = () => { router.back(); }; -// 发送消息 -const sendMessage = async () => { - if (!inputMessage.value.trim() || isRequestPending.value) return proxy.$message.warning("请输入内容"); - isRequestPending.value = true; - isTypingComplete.value = true; // 重置为初始状态 - // 重置状态 - // 添加用户消息 - messages.value.push({ - id: Date.now(), - role: "user", - content: inputMessage.value, - }); - // 用户提问后立即滚动到底部 - nextTick(() => { - scrollToBottom(); - inputMessage.value = ""; - }); +// 通过 AI 对话通道(api/kimi4)发起流式提问,AI 回复以气泡逐字输出 +const openAssistantStream = async (content) => { abortController = new AbortController(); const signal = abortController.signal; // 调用后端API - let params = { + const params = { systemOwner: "电商互联网产品", userId: userStore.userInfo.userId, type: "2", }; + isRequestPending.value = true; isTypingComplete.value = false; // 将参数对象转换为查询字符串 const queryString = new URLSearchParams(params); @@ -127,14 +113,15 @@ const sendMessage = async () => { headers: { "Content-Type": "application/json", }, - body: inputMessage.value.trim(), + body: content, signal, }); + if (!resp.ok || !resp.body) { + throw new Error(`AI服务响应异常(${resp.status})`); + } // 添加AI消息占位 - let newMessage = reactive({ id: Date.now(), role: "assistant", content: "" }); - messages.value.push(newMessage); // 滚动到底部 - console.log(messages.value, " messages.value"); - + const newMessage = reactive({ id: Date.now(), role: "assistant", content: "" }); + messages.value.push(newMessage); // 处理流式响应 const reader = resp.body.getReader(); const decoder = new TextDecoder("utf-8"); @@ -146,11 +133,35 @@ const sendMessage = async () => { await typeText(newMessage, text); //滚动到最新消息 nextTick(() => { - const scrollEl = scrollArea.value.wrapRef; - scrollEl.scrollTop = scrollEl.scrollHeight; + scrollToBottom(); }); } }; +// 发送消息 +const sendMessage = async () => { + if (!inputMessage.value.trim() || isRequestPending.value) return proxy.$message.warning("请输入内容"); + // 添加用户消息 + messages.value.push({ + id: Date.now(), + role: "user", + content: inputMessage.value, + }); + // 用户提问后立即滚动到底部 + nextTick(() => { + scrollToBottom(); + inputMessage.value = ""; + }); + try { + await openAssistantStream(inputMessage.value.trim()); + } catch (error) { + if (error?.name !== "AbortError") { + proxy.$message.error("AI服务暂时不可用,请稍后重试"); + } + } finally { + isRequestPending.value = false; + isTypingComplete.value = true; + } +}; const typeText = (message, text) => { return new Promise((resolve) => { isTypingComplete.value = false; // 重置状态 @@ -217,31 +228,55 @@ const gInterrupt = () => { // 获取分析 const getAnalysis = async () => { loading.value = true; + const stuNeed = proxy.$cache.session.getJSON("msg"); + const stuData = proxy.$cache.session.getJSON("learningAnalysisData"); try { - const res = await AI.getLearningAnalysis({ stuNeed: proxy.$cache.session.getJSON("msg"), stuData: proxy.$cache.session.getJSON("learningAnalysisData") }); + const res = await AI.getLearningAnalysis({ stuNeed, stuData }); + const content = typeof res === "string" ? res : res?.data ?? ""; messages.value.push({ id: Date.now(), role: "assistant", - content: res, + content, }); } catch (error) { - proxy.$message.error("AI学情分析暂时不可用,请稍后重试"); + // 专用学情分析接口不可用时,把同一条分析需求和学情数据交给 AI 对话通道分析 + try { + loading.value = false; + await openAssistantStream(`${stuNeed}\n请严格基于以下学情数据(JSON)分析,不要补造数据:\n${JSON.stringify(stuData)}`); + } catch (fallbackError) { + lastAnalysisRequest = null; // 两条通道都失败,允许用户重试同一条分析 + if (fallbackError?.name !== "AbortError") { + proxy.$message.error("AI学情分析暂时不可用,请稍后重试"); + } + } } finally { loading.value = false; } }; -onMounted(() => { - if (proxy.$cache.session.getJSON("msg") && proxy.$cache.session.getJSON("learningAnalysisData")) { +// 已渲染过的分析请求:页面被 keep-alive 缓存后回流会再次触发入口逻辑, +// 同一条分析请求不重复发起(对话内容本身仍保留在页面上)。 +let lastAnalysisRequest = null; +// 入口逻辑:带学情数据则发起分析;带教案请求则作为提问发送 +const runEntryFlow = () => { + const stuNeed = proxy.$cache.session.getJSON("msg"); + const stuData = proxy.$cache.session.getJSON("learningAnalysisData"); + if (stuNeed && stuData) { + if (stuNeed === lastAnalysisRequest && (loading.value || messages.value.length)) return; + lastAnalysisRequest = stuNeed; getAnalysis(); - } else { - if (proxy.$cache.session.getJSON("teachingPlan")) { - inputMessage.value = proxy.$cache.session.getJSON("teachingPlan"); - sendMessage(); - // 删除我提问的这条 - messages.value.splice(0, 1); - } + return; } -}); + if (proxy.$cache.session.getJSON("teachingPlan")) { + inputMessage.value = proxy.$cache.session.getJSON("teachingPlan"); + // 用后即清,避免 keep-alive 回流时重复发送 + proxy.$cache.session.remove("teachingPlan"); + sendMessage(); + // 删除我提问的这条 + messages.value.splice(0, 1); + } +}; +onMounted(runEntryFlow); +onActivated(runEntryFlow); // 导出方法 const exportData = () => { diff --git a/src/views/components/TrainingAiEvaluation.vue b/src/views/components/TrainingAiEvaluation.vue index 1e6ef08..854b392 100644 --- a/src/views/components/TrainingAiEvaluation.vue +++ b/src/views/components/TrainingAiEvaluation.vue @@ -63,7 +63,7 @@ diff --git a/src/views/teacherEnd/class/index.vue b/src/views/teacherEnd/class/index.vue index cb96973..4148d4d 100644 --- a/src/views/teacherEnd/class/index.vue +++ b/src/views/teacherEnd/class/index.vue @@ -24,7 +24,7 @@
- + @@ -464,6 +464,12 @@ function getList(options = {}) { }) } +// 查询回到第一页,避免停留在超出结果范围的页码 +function search() { + paramsquery.value.index = 1 + getList() +} + function handleTabChange() { paramsquery.value.index = 1 getList() diff --git a/src/views/teacherEnd/major/index.vue b/src/views/teacherEnd/major/index.vue index ba3fe43..3056b46 100644 --- a/src/views/teacherEnd/major/index.vue +++ b/src/views/teacherEnd/major/index.vue @@ -18,7 +18,7 @@
专业名称: - + @@ -161,6 +161,11 @@ const getList = () => { schoolTotal.value = res.data.total; }); }; +// 搜索回到第一页,避免停留在超出结果范围的页码 +const search = () => { + paramsquery.value.index = 1; + getList(); +}; // 删除院校 const delSchool = (id) => { indexApi.deleteMajor({ userId: userStore.userInfo.userId, schoolMajorId: id }).then((res) => { diff --git a/src/views/teacherEnd/school/index.vue b/src/views/teacherEnd/school/index.vue index 8d692bb..a15929a 100644 --- a/src/views/teacherEnd/school/index.vue +++ b/src/views/teacherEnd/school/index.vue @@ -12,7 +12,7 @@
院系名称: - + @@ -156,6 +156,11 @@ const getList = () => { schoolTotal.value = res.data.total; }); }; +// 搜索回到第一页,避免停留在超出结果范围的页码 +const search = () => { + paramsquery.value.index = 1; + getList(); +}; // 删除院校 const delSchool = (id) => { indexApi.deleteSchool({ schoolFacultyId: id, userId: userStore.userInfo.userId }).then((res) => { diff --git a/src/views/teacherEnd/student/index.vue b/src/views/teacherEnd/student/index.vue index bff1c41..6e1994d 100644 --- a/src/views/teacherEnd/student/index.vue +++ b/src/views/teacherEnd/student/index.vue @@ -23,7 +23,7 @@
学生学号: - + @@ -130,23 +130,6 @@