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 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -224,9 +207,8 @@ const createEmptyForm = () => ({
userName: "",
name: "",
})
+// 院系/专业选项已停用,新增/编辑只需选择本校行政班
const organizationRules = {
- schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }],
- schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }],
schoolClassId: [{ required: true, message: "请选择所属班级", trigger: "change" }],
}
const studentIdentityRules = {
@@ -262,6 +244,11 @@ const getList = () => {
schoolTotal.value = res.data.total
})
}
+// 搜索回到第一页,避免停留在超出结果范围的页码
+const search = () => {
+ paramsquery.value.index = 1
+ getList()
+}
// 删除院校
const delSchool = (id) => {
indexApi.deleteStudent({ userId: id.userId, operatorId: currentUserId.value, teachingClassId: resolveRowTeachingClassId(id) }).then((res) => {
@@ -270,12 +257,14 @@ const delSchool = (id) => {
})
}
// 新增院校
-const addschool = () => {
+const addschool = async () => {
formInline.value = createEmptyForm()
classInfo.value = {}
- classList1.value = []
dialogVisible.value = true
status.value = true
+ // 班级下拉不再依赖院系/专业联动,直接列出本校可用的行政班
+ await getClassList().catch(() => null)
+ classList1.value = classList.value.filter((item) => canUseAdminClassForStudent(item))
}
const editSchool = (row) => {
console.log(row)
@@ -288,12 +277,6 @@ const editSchool = (row) => {
}
dialogVisible.value = true
status.value = false
- if (!isTeacherRosterMode.value && row.schoolMajorId) {
- indexApi.getFacultyAndMajorName({ schoolMajorId: row.schoolMajorId }).then((res) => {
- classInfo.value.name = Object.keys(res.data)[0]
- classInfo.value.value = Object.values(res.data)[0]
- })
- }
}
const add = () => {
formInline.value.schoolId = currentSchoolId.value
@@ -306,7 +289,6 @@ const add = () => {
})
return
}
- formInline.value.schoolMajorName = majorList.value?.filter((item) => item.schoolMajorId == formInline.value.schoolMajorId)[0].schoolMajorName
formInline.value.schoolClassName = classList1.value?.filter((item) => item.schoolClassId == formInline.value.schoolClassId)[0].className
formInline.value.userId = currentUserId.value
indexApi.addStudent(formInline.value).then((res) => {
@@ -357,22 +339,6 @@ const refresh = () => {
getList()
}, 2000)
}
-// 查询院系下拉列表
-const facultyList = ref([])
-const getFacultyList = () => {
- indexApi.seleteSchoolFacultyList({ schoolId: userStore.userInfo.schoolId }).then((res) => {
- facultyList.value = res.data
- })
-}
-// 专业下拉框
-const majorList = ref([])
-const getMajorList = () => {
- if (!formInline.value.schoolFacultyId) return
- paramsquery.value.schoolMajorId = ""
- indexApi.getMajorListBySchoolId({ schoolFacultyId: formInline.value.schoolFacultyId }).then((res) => {
- majorList.value = res.data
- })
-}
const uploadStudents = (options) => {
if (isTeacherRosterMode.value) {
const formData = new FormData()
@@ -407,25 +373,12 @@ const exportTeacher = () => {
})
}
// 导入模版下载
-const buildOwnedAdminClassRows = async () => {
- const majorGroups = await Promise.all(
- facultyList.value.map((faculty) =>
- indexApi
- .getMajorListBySchoolId({ schoolFacultyId: faculty.schoolFacultyId })
- .then((res) => (res.data || []).map((major) => ({ ...major, schoolFacultyName: faculty.schoolFacultyName })))
- .catch(() => []),
- ),
- )
- const allMajors = majorGroups.flat()
- return classList.value
+// 班级下拉与导入模板不再区分院系/专业,仅列出本校行政班名称
+const ownedAdminClassNames = () =>
+ classList.value
.filter((item) => canUseAdminClassForStudent(item))
- .map((item) => {
- const major = allMajors.find((majorItem) => majorItem.schoolMajorId === item.schoolMajorId) || majorList.value.find((majorItem) => majorItem.schoolMajorId === item.schoolMajorId)
- const faculty = facultyList.value.find((facultyItem) => facultyItem.schoolFacultyId === (major?.schoolFacultyId || item.schoolFacultyId))
- return [faculty?.schoolFacultyName || item.schoolFacultyName || "", major?.schoolMajorName || item.schoolMajorName || "", item.className || ""]
- })
- .filter((row) => row.every(Boolean))
-}
+ .map((item) => item.className || "")
+ .filter(Boolean)
const downloadTemplate = async () => {
const XLSX = await import("xlsx")
@@ -441,17 +394,15 @@ const downloadTemplate = async () => {
XLSX.writeFile(workbook, "学生账号导入模板.xlsx")
return
}
- const adminClassRows = await buildOwnedAdminClassRows()
- const firstAdminClass = adminClassRows[0] || ["电子商务学院", "电子商务", "行政一班"]
+ const classNames = ownedAdminClassNames()
const templateSheet = XLSX.utils.aoa_to_sheet([
- ["姓名", "学号", "性别", "所属院系", "所属专业", "所属班级"],
- ["张三", "20260001", "男", firstAdminClass[0], firstAdminClass[1], firstAdminClass[2]],
+ ["姓名", "学号", "性别", "所属班级"],
+ ["张三", "20260001", "男", classNames[0] || "行政一班"],
])
- templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 24 }, { wch: 24 }, { wch: 20 }]
- const dataRows = adminClassRows.length ? adminClassRows : [["暂无可导入行政班", "请先维护专业", "请先创建行政班"]]
- const dataSheet = XLSX.utils.aoa_to_sheet([["所属院系", "所属专业", "所属班级"], ...dataRows])
- dataSheet["!cols"] = [{ wch: 28 }, { wch: 28 }, { wch: 24 }]
- const guideSheet = XLSX.utils.aoa_to_sheet([["填写说明"], ["1. 本模板用于学生账号导入,不要填写任何数据ID。"], ["2. 所属院系、所属专业、所属班级请使用基础数据页中的名称。"], ["3. 只能导入本校行政班,不能导入教学班或其他学校的班级。"], ["4. 性别可填写男、女,也可留空。"], ["5. 任意一行校验失败时,整批导入失败并返回失败原因。"]])
+ templateSheet["!cols"] = [{ wch: 14 }, { wch: 18 }, { wch: 10 }, { wch: 20 }]
+ const dataSheet = XLSX.utils.aoa_to_sheet([["所属班级"], ...classNames.map((name) => [name])])
+ dataSheet["!cols"] = [{ wch: 24 }]
+ const guideSheet = XLSX.utils.aoa_to_sheet([["填写说明"], ["1. 本模板用于学生账号导入,不要填写任何数据ID。"], ["2. 所属班级请使用基础数据页中的名称。"], ["3. 只能导入本校行政班,不能导入教学班或其他学校的班级。"], ["4. 性别可填写男、女,也可留空。"], ["5. 任意一行校验失败时,整批导入失败并返回失败原因。"]])
guideSheet["!cols"] = [{ wch: 76 }]
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, templateSheet, "学生导入模板")
@@ -491,14 +442,6 @@ const getClassList = () => {
})
}
const classList1 = ref([])
-// 根据专业查班级
-const getClassByMajor = () => {
- if (!formInline.value.schoolMajorId) return
- formInline.value.schoolClassId = ""
- indexApi.getClassListByMajorId({ schoolMajorId: formInline.value.schoolMajorId }).then((res) => {
- classList1.value = (res.data || []).filter((item) => canUseAdminClassForStudent(item))
- })
-}
//初始化密码
const initPassword = (row, status) => {
if (status == 0) {
@@ -542,9 +485,6 @@ const initPassword = (row, status) => {
}
onMounted(async () => {
await userStore.ensureSchoolProductConfig().catch(() => null)
- if (!isTeacherRosterMode.value) {
- getFacultyList()
- }
await getClassList()
// 默认展示第一个班级数据:把 select 绑到第一个教学班并立即拉取学生列表
if (!paramsquery.value.schoolClassId && studentClassOptions.value.length) {
diff --git a/src/views/teacherEnd/teacher/index.vue b/src/views/teacherEnd/teacher/index.vue
index 710a3b8..6385ade 100644
--- a/src/views/teacherEnd/teacher/index.vue
+++ b/src/views/teacherEnd/teacher/index.vue
@@ -16,7 +16,7 @@
教师工号:
-
+
@@ -246,7 +246,8 @@ const organizationRules = {
schoolMajorId: [{ required: true, message: "请选择专业", trigger: "change" }],
}
const rules = computed(() => ({
- ...((status.value || isOrganizationManagementEnabled.value) ? organizationRules : {}),
+ // 院系/专业新增时已停用;仅编辑且展示只读归属时才保留规则
+ ...((!status.value && isOrganizationManagementEnabled.value) ? organizationRules : {}),
name: [{ required: true, message: "请输入教师姓名", trigger: "blur" }],
userName: [{ required: true, message: "请输入教师工号", trigger: "blur" }],
}))
@@ -268,6 +269,11 @@ const getList = () => {
schoolTotal.value = res.data.total
})
}
+// 搜索回到第一页,避免停留在超出结果范围的页码
+const search = () => {
+ paramsquery.value.index = 1
+ getList()
+}
// 删除院校
const delSchool = (id) => {
indexApi.deleteTeacher({ userId: id.userId, operatorId: currentUserId.value }).then((res) => {
@@ -388,14 +394,12 @@ const exportTeacher = () => {
// 导入模板下载
const downloadTemplate = async () => {
const XLSX = await import("xlsx")
+ // 教师的学校/院系/专业直接继承当前操作用户,模板不再需要组织信息列
const templateSheet = XLSX.utils.aoa_to_sheet([
- ["院校名称", "院系名称", "专业名称", "教师姓名", "工号", "电话", "邮箱"],
- ["请填写系统中的院校名称", "请填写系统中的院系名称", "请填写系统中的专业名称", "张老师", "20260001", "13800000000", "teacher@example.com"],
+ ["教师姓名", "工号", "电话", "邮箱"],
+ ["张老师", "20260001", "13800000000", "teacher@example.com"],
])
templateSheet["!cols"] = [
- { wch: 26 },
- { wch: 24 },
- { wch: 24 },
{ wch: 16 },
{ wch: 18 },
{ wch: 18 },
@@ -403,8 +407,8 @@ const downloadTemplate = async () => {
]
const guideSheet = XLSX.utils.aoa_to_sheet([
["填写说明"],
- ["1. 院校、院系和专业名称必须与系统中已维护的名称完全一致。"],
- ["2. 教师姓名和工号必填,工号只能填写数字且不能重复。"],
+ ["1. 教师姓名和工号必填,工号只能填写数字且不能重复。"],
+ ["2. 导入的教师归入你所在的学校(院系/专业同你的账号)。"],
["3. 电话和邮箱可以留空;填写时必须符合手机号和邮箱格式。"],
["4. 默认密码为 123qwe,导入成功后请提醒教师及时修改密码。"],
])
diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue
index 511f727..17b7800 100644
--- a/src/views/teacherEnd/trainingTask/index.vue
+++ b/src/views/teacherEnd/trainingTask/index.vue
@@ -26,7 +26,7 @@
-
+
@@ -327,7 +327,6 @@
@@ -426,13 +425,12 @@ const restoring = ref(false)
const uploadingField = ref("")
const choiceEditorVisible = ref(false)
const bankPickerVisible = ref(false)
-// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段
-// accept 为空表示不限制文件类型,否则按逗号分隔的扩展名校验
+// 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段,不限制文件类型
const materialSlots = [
- { key: "material", label: "案例数据", accept: ".xls,.xlsx" },
- { key: "trainingData", label: "案例数据", accept: ".xls,.xlsx" },
+ { key: "material", label: "案例资料" },
+ { key: "trainingData", label: "案例视频" },
{ key: "guideManual", label: "实训指导手册" },
- { key: "aiManual", label: "AI辅助指导手册" },
+ { key: "aiManual", label: "AI排错指导手册" },
]
const tasks = ref([])
const classList = ref([])
@@ -743,19 +741,6 @@ function removeObjective(index) {
async function handleMaterialChange(slotKey, file) {
const rawFile = file?.raw
if (!rawFile) return
- const slot = materialSlots.find((item) => item.key === slotKey)
- if (slot?.accept) {
- const allowedExts = slot.accept
- .split(",")
- .map((item) => item.trim().toLowerCase())
- .filter(Boolean)
- const fileName = String(rawFile.name || file?.name || "").toLowerCase()
- const isAllowed = allowedExts.some((ext) => fileName.endsWith(ext))
- if (!isAllowed) {
- proxy?.$modal?.msgError?.(`${slot.label}仅支持 ${allowedExts.join(" / ")} 格式`)
- return
- }
- }
const data = new FormData()
data.append("file", rawFile)
uploadingField.value = slotKey
diff --git a/tests/teacher-import-template.static.test.cjs b/tests/teacher-import-template.static.test.cjs
index 2acc7e7..912e710 100644
--- a/tests/teacher-import-template.static.test.cjs
+++ b/tests/teacher-import-template.static.test.cjs
@@ -9,7 +9,11 @@ const page = fs.readFileSync(
assert(/const downloadTemplate = async \(\) =>/.test(page));
assert(/await import\("xlsx"\)/.test(page));
-assert(/\["院校名称", "院系名称", "专业名称", "教师姓名", "工号", "电话", "邮箱"\]/.test(page));
+// 2026-10-12 起新增教师不再采集院校/院系/专业,归属直接继承当前操作用户,模板同步移除对应列
+assert(/\["教师姓名", "工号", "电话", "邮箱"\]/.test(page));
+assert(!/院校名称/.test(page));
+assert(!/院系名称/.test(page));
+assert(!/专业名称/.test(page));
assert(/XLSX\.utils\.book_append_sheet\(workbook, templateSheet, "教师导入模板"\)/.test(page));
assert(/XLSX\.writeFile\(workbook, "教师账号导入模板\.xlsx"\)/.test(page));
assert(!/\/file\/教师信息表格\.xlsx/.test(page));
diff --git a/tests/teacher-student-roster-mode.static.test.cjs b/tests/teacher-student-roster-mode.static.test.cjs
index fed95f5..1a0d61b 100644
--- a/tests/teacher-student-roster-mode.static.test.cjs
+++ b/tests/teacher-student-roster-mode.static.test.cjs
@@ -16,8 +16,8 @@ assert(page.includes('v-if="isTeacherRosterMode" label="所属教学班:" prop="
assert(page.includes('teachingClassId: [{ required: true, message: "请选择所属教学班", trigger: "change" }]'), "教师维护名单模式必须校验教学班必选");
assert(page.includes('teachingClassOptions'), "教师维护名单模式必须只提供可维护的教学班选项");
assert(
- /if \(!isTeacherRosterMode\.value && row\.schoolMajorId\) \{\s*indexApi\.getFacultyAndMajorName/.test(page),
- "教师维护名单模式不能请求必填 schoolMajorId 的组织结构接口"
+ !page.includes("getFacultyAndMajorName"),
+ "院系/专业停用后,任何模式都不应再请求必填 schoolMajorId 的组织结构接口"
);
assert(page.includes("indexApi.addTeacherRosterStudent(formInline.value)"), "教师维护名单模式新增学生必须使用无组织字段的名单接口");
assert(page.includes("indexApi.updateTeacherRosterStudent(formInline.value)"), "教师维护名单模式编辑学生必须使用无班级归属的名单接口");