feat: 完善学校配置与教师演示模式

dev-QQq
chenyuan 4 weeks ago
parent 37d4d184bc
commit 83abcba523

@ -485,6 +485,30 @@ export function addStudent(params) {
params,
});
}
export function addTeacherRosterStudent(data) {
return request({
url: "/api/user/teacher-roster/students",
method: "POST",
params: {
name: data.name,
userName: data.userName,
teachingClassId: data.teachingClassId,
operatorId: data.operatorId,
},
});
}
export function updateTeacherRosterStudent(data) {
return request({
url: `/api/user/teacher-roster/students/${data.userId}`,
method: "POST",
params: {
name: data.name,
userName: data.userName,
teachingClassId: data.teachingClassId,
operatorId: data.operatorId,
},
});
}
// 学生管理 编辑
export function updateStudent(data) {
const { operatorId, teachingClassId, ...body } = data;

@ -95,15 +95,15 @@
color: var(--el-color-primary) !important;
}
.app-container .el-table .el-loading-mask {
background: rgba(4, 18, 29, 0.72) !important;
backdrop-filter: blur(1px);
.el-loading-mask {
background: rgba(4, 18, 29, 0.78) !important;
backdrop-filter: blur(2px);
}
.app-container .el-table .el-loading-spinner .path {
.el-loading-spinner .path {
stroke: #20d6ff;
}
.app-container .el-table .el-loading-text {
.el-loading-text {
color: #d7f8ff;
}

@ -33,7 +33,14 @@ const router = useRouter();
const role = ref(JSON.parse(getUserInfo()).roleId);
const isDemo = computed(() => isStudentDemo());
const demoClassName = computed(() => getStudentDemoSession()?.userInfo?.className || "教学班");
function exitDemo() { clearStudentDemoSession(); window.location.replace("/student-demo"); }
function exitDemo() {
clearStudentDemoSession();
window.close();
//
window.setTimeout(() => {
if (!window.closed) window.location.replace("/index");
}, 100);
}
const mainClass = computed(() => {
if (Number(role.value) === 4) return "app-main";
if (Number(role.value) === 3) return "teacher-main";
@ -314,6 +321,7 @@ router.beforeEach(async (to, from, next) => {
--el-table-border-color: rgba(54, 159, 212, 0.2);
--el-table-header-bg-color: rgba(9, 48, 73, 0.96);
--el-table-tr-bg-color: rgba(2, 16, 27, 0.78);
--el-fill-color-lighter: rgba(2, 16, 27, 0.72);
--el-table-row-hover-bg-color: rgba(0, 111, 177, 0.18);
margin-top: 16px !important;
color: #dff4ff;

@ -78,9 +78,10 @@ import HeaderSearch from "@/components/HeaderSearch";
import RuoYiGit from "@/components/RuoYi/Git";
import RuoYiDoc from "@/components/RuoYi/Doc";
import useAppStore from "@/store/modules/app";
import useUserStore from "@/store/modules/user";
import useSettingsStore from "@/store/modules/settings";
import * as indexApi from "@/api/teacher";
import useUserStore from "@/store/modules/user";
import useSettingsStore from "@/store/modules/settings";
import * as indexApi from "@/api/teacher";
import { getStudentDemoSession, isStudentDemo } from "@/utils/studentDemo";
const { proxy } = getCurrentInstance();
const router = useRouter();
const appStore = useAppStore();
@ -275,6 +276,19 @@ const roleList = computed(() => {
});
//
const getUserInfo = () => {
if (isStudentDemo()) {
const demoUser = getStudentDemoSession()?.userInfo || userStore.userInfo;
form.value = {
name: demoUser.name || "演示学生",
username: demoUser.username || "",
schoolFacultyId: "演示模式",
schoolMajorId: "演示模式",
schoolClassId: demoUser.className || "教学班",
password: "******",
passwords: "",
};
return;
}
if (role.value === 1) {
form.value = {
name: userStore.userInfo.name,
@ -295,8 +309,12 @@ const getUserInfo = () => {
.catch(() => {});
};
//
const changePassword = () => {
indexApi
const changePassword = () => {
if (isStudentDemo()) {
proxy.$modal.msgWarning("演示模式不支持修改密码");
return;
}
indexApi
.updatePassword({ userId: userStore.userInfo.userId, password: form.value.passwords })
.then(() => {
proxy.$modal.msgSuccess("修改成功");

@ -52,8 +52,8 @@
</el-scrollbar>
</template>
<el-scrollbar v-else :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu :default-openeds="['/demand/demand', '/product/product', '/test/test', '/channel/channel', '/assessment/assessment']" :default-active="activeMenu" :collapse="isCollapse" :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" :unique-opened="false" :active-text-color="theme" :collapse-transition="false" mode="vertical">
<sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
<el-menu :key="sidebarMenuKey" :default-openeds="['/demand/demand', '/product/product', '/test/test', '/channel/channel', '/assessment/assessment']" :default-active="activeMenu" :collapse="isCollapse" :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" :unique-opened="false" :active-text-color="theme" :collapse-transition="false" mode="vertical">
<sidebar-item v-for="(route, index) in visibleSidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
@ -76,9 +76,11 @@ const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const sidebarRouters = ref([...constantRoutes]);
const routerStatus = {
const permissionStore = usePermissionStore();
const sidebarRouters = ref([...constantRoutes]);
const visibleSidebarRouters = computed(() => filterRoutesByOrganizationMode(sidebarRouters.value));
const sidebarMenuKey = computed(() => visibleSidebarRouters.value.map((route) => route.path).join("|"));
const routerStatus = {
行业需求分析: [
{
id: "9f413263-e6ff-4ee5-984a-7a231cad6818",
@ -337,8 +339,8 @@ const teacherManageNav = [
{
title: "教学管理",
children: [
{ title: "院系管理", path: "/school/index" },
{ title: "专业管理", path: "/major/index" },
{ title: "院系管理", path: "/school/index", requiresFacultyManagement: true },
{ title: "专业管理", path: "/major/index", requiresMajorManagement: true },
{ title: "班级管理", path: "/class/index" },
{ title: "教师管理", path: "/teacher/index" },
{ title: "学生管理", path: "/student/index" },
@ -374,7 +376,7 @@ const trainingTaskKeyByPath = {
"/assessment/diagnosis": "product-diagnosis-iteration",
};
const trainingTaskTitleByPath = ref({});
const currentCourseNav = computed(() => (isTeacherRole.value ? teacherManageNav : studentCourseNav));
const currentCourseNav = computed(() => (isTeacherRole.value ? filterTeacherManageNavigation(teacherManageNav) : studentCourseNav));
const studentIconByPath = {
"/assistant/index": "MagicStick",
"/foundation/new-product-survey": "Search",
@ -561,10 +563,11 @@ function excludeRoutesByMetaTitle(routes, excludeTitles) {
return result;
}, []);
}
const getTaskList = () => {
const getTaskList = async () => {
if (userStore.userInfo.roleId == 1) {
sidebarRouters.value = [...constantRoutes, ...platformAdminRoutes];
} else if (userStore.userInfo.roleId == 2) {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(schoolAdminRoutes)];
} else if (userStore.userInfo.roleId != 3) {
indexApi.getStudentTaskList({ userId: userStore.userInfo.userId }).then((res) => {
@ -572,26 +575,41 @@ const getTaskList = () => {
console.log(processModuleStatus(groupAndSortModules(res.data)));
proxy.$cache.session.setJSON("disabledChildren", processModuleStatus(groupAndSortModules(res.data)).disabledChildren);
proxy.$cache.session.setJSON("disabledTitles", disabledTitles);
sidebarRouters.value = [...constantRoutes, ...dynamicRoutes];
});
sidebarRouters.value = [...constantRoutes, ...dynamicRoutes];
});
} else {
await userStore.ensureSchoolProductConfig().catch(() => null);
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(teacherRoutes)];
}
};
function filterRoutesByOrganizationMode(routes) {
function isSchoolNavigationVisible(item) {
const config = userStore.schoolProductConfig;
const legacyAdminEnabled = config?.organizationMode !== "TEACHING_CLASS_ONLY";
const facultyEnabled = config?.facultyManagementEnabled ?? legacyAdminEnabled;
const majorEnabled = config?.majorManagementEnabled ?? legacyAdminEnabled;
const adminClassEnabled = config?.adminClassManagementEnabled ?? legacyAdminEnabled;
return routes.filter((route) => {
if (route.meta?.requiresManagerTeacher && userStore.userInfo.teacherAdmin !== true) return false;
if (route.meta?.requiresFacultyManagement && !facultyEnabled) return false;
if (route.meta?.requiresMajorManagement && !majorEnabled) return false;
if (route.meta?.requiresAdminClass && !adminClassEnabled) return false;
return true;
});
const facultyEnabled = config?.facultyManagementEnabled === true;
const majorEnabled = config?.majorManagementEnabled === true;
const adminClassEnabled = config?.adminClassManagementEnabled === true;
const capability = item.meta || item;
if (capability.requiresManagerTeacher && userStore.userInfo.teacherAdmin !== true) return false;
if (capability.requiresFacultyManagement && !facultyEnabled) return false;
if (capability.requiresMajorManagement && !majorEnabled) return false;
if (capability.requiresAdminClass && !adminClassEnabled) return false;
return true;
}
function filterRoutesByOrganizationMode(routes) {
return routes.filter(isSchoolNavigationVisible);
}
function filterTeacherManageNavigation(sections) {
return sections
.map((section) => {
if (!section.children) return section;
return {
...section,
children: section.children.filter(isSchoolNavigationVisible),
};
})
.filter((section) => (section.children ? section.children.length > 0 : isSchoolNavigationVisible(section)));
}
watch(
() => userStore.schoolProductConfig?.organizationMode,

@ -511,7 +511,7 @@ export const teacherRoutes = [
component: Layout,
redirect: "/teacher/index",
roles: ["teacher"],
meta: { title: "教师管理", icon: "教师管理", affix: true, requiresAdminClass: true },
meta: { title: "教师管理", icon: "教师管理", affix: true },
children: [
// 教师管理
{

@ -68,7 +68,7 @@ const useUserStore = defineStore("user", {
if (this.schoolProductConfig) {
return Promise.resolve(this.schoolProductConfig);
}
if (!this.userInfo?.schoolId || this.userInfo?.roleId == 1) {
if (this.userInfo?.roleId == 1) {
return Promise.resolve(null);
}
return getCurrentSchoolProductConfig().then((res) => {

@ -179,8 +179,8 @@ getCookie();
-webkit-text-fill-color: #303133;
}
:deep(.password-input .el-input__inner::placeholder),
:deep(.password-input input::placeholder) {
:deep(.el-input__inner::placeholder),
:deep(input::placeholder) {
color: #b8c0cc;
-webkit-text-fill-color: #b8c0cc;
opacity: 1;

@ -23,12 +23,12 @@
<el-table :data="filteredTableData" border v-loading="loading" class="class-table" :header-cell-style="tableHeaderStyle">
<el-table-column label="班级名称" prop="schoolClass.className" align="center" />
<el-table-column label="所属院系" align="center">
<el-table-column v-if="facultyManagementEnabled" label="所属院系" align="center">
<template #default="{ row }">
<span>{{ row.schoolFacultyName || "-" }}</span>
</template>
</el-table-column>
<el-table-column label="所属专业" align="center">
<el-table-column v-if="majorManagementEnabled" label="所属专业" align="center">
<template #default="{ row }">
<span>{{ row.schoolMajorName || "-" }}</span>
</template>

@ -77,7 +77,8 @@
</el-table>
<pagination v-show="schoolTotal > 0" :total="schoolTotal" v-model:page="paramsquery.index" v-model:limit="paramsquery.size" @pagination="getList" />
<el-dialog v-model="dialogVisible" width="560px" :title="status ? '新增学生' : '编辑学生'" class="teacher-dialog" @close="onClose">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px" style="width: 80%" :rules="rules">
<el-form :model="formInline" class="demo-form-inline" ref="formInlineref" label-width="100px" style="width: 80%" :rules="studentFormRules">
<template v-if="!isTeacherRosterMode">
<el-form-item label="所属院系:" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="" clearable @change="getMajorList" style="width: 20rem" v-if="status">
<el-option v-for="school in facultyList" :key="school.schoolId" :label="school.schoolFacultyName" :value="school.schoolFacultyId" />
@ -96,6 +97,12 @@
</el-select>
<el-input v-else v-model="classInfo.className" placeholder="请输入所属班级:" style="width: 20rem" disabled></el-input>
</el-form-item>
</template>
<el-form-item v-if="isTeacherRosterMode" label="所属教学班:" prop="teachingClassId">
<el-select v-model="formInline.teachingClassId" placeholder="请选择教学班" clearable style="width: 20rem">
<el-option v-for="schoolClass in teachingClassOptions" :key="schoolClass.schoolClassId" :label="schoolClass.className" :value="schoolClass.schoolClassId" />
</el-select>
</el-form-item>
<el-form-item label="学生姓名:" prop="name">
<el-input v-model="formInline.name" style="width: 20rem" placeholder=""></el-input>
</el-form-item>
@ -121,6 +128,7 @@ import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
const currentUserId = computed(() => userStore.userInfo.userId);
const currentSchoolId = computed(() => userStore.userInfo.schoolId);
const isTeacherRosterMode = computed(() => userStore.schoolProductConfig?.studentRosterOwner === "TEACHER");
import * as indexApi from "@/api/teacher";
const dialogVisible = ref(false);
const loading = ref(false);
@ -130,6 +138,7 @@ const formInline = ref({
schoolMajorId: "",
schoolMajorName: "",
schoolClassId: "",
teachingClassId: "",
schoolName: "",
userName: "",
});
@ -139,17 +148,25 @@ const createEmptyForm = () => ({
schoolMajorId: "",
schoolMajorName: "",
schoolClassId: "",
teachingClassId: "",
schoolName: "",
userName: "",
name: "",
});
const rules = ref({
const organizationRules = {
schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }],
schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }],
schoolClassId: [{ required: true, message: "请选择所属班级", trigger: "change" }],
};
const studentIdentityRules = {
name: [{ required: true, message: "请输入学生姓名", trigger: "blur" }],
userName: [{ required: true, message: "请输入学号", trigger: "blur" }],
});
};
const teacherRosterRules = {
...studentIdentityRules,
teachingClassId: [{ required: true, message: "请选择所属教学班", trigger: "change" }],
};
const studentFormRules = computed(() => (isTeacherRosterMode.value ? teacherRosterRules : { ...organizationRules, ...studentIdentityRules }));
const paramsquery = ref({
index: 1,
size: 10,
@ -194,18 +211,30 @@ const editSchool = (row) => {
let data = { ...row };
formInline.value = data;
formInline.value.userName = row.username;
formInline.value.teachingClassId = isTeacherRosterMode.value ? (row.teachingClassId || currentTeachingClassId()) : "";
classInfo.value = {
className: row.className || row.schoolClassName || "",
};
dialogVisible.value = true;
status.value = false;
indexApi.getFacultyAndMajorName({ schoolMajorId: row.schoolMajorId }).then((res) => {
classInfo.value.name = Object.keys(res.data)[0];
classInfo.value.value = Object.values(res.data)[0];
});
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;
formInline.value.operatorId = currentUserId.value;
if (isTeacherRosterMode.value) {
indexApi.addTeacherRosterStudent(formInline.value).then((res) => {
proxy.$message.success("添加成功");
dialogVisible.value = false;
getList();
});
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;
@ -218,6 +247,14 @@ const add = () => {
const edit = () => {
formInline.value.status = true;
formInline.value.operatorId = currentUserId.value;
if (isTeacherRosterMode.value) {
indexApi.updateTeacherRosterStudent(formInline.value).then((res) => {
proxy.$message.success("编辑成功");
dialogVisible.value = false;
getList();
});
return;
}
formInline.value.teachingClassId = currentTeachingClassId();
indexApi.updateStudent(formInline.value).then((res) => {
proxy.$message.success("编辑成功");
@ -352,6 +389,14 @@ const canUseAdminClassForStudent = (schoolClass) => {
};
const classMap = computed(() => new Map(classList.value.map((item) => [item.schoolClassId, item])));
const adminClassList = computed(() => classList.value.filter((item) => canUseAdminClassForStudent(item)));
const teachingClassOptions = computed(() =>
classList.value.filter(
(schoolClass) =>
normalizeClassType(schoolClass) === "TEACHING" &&
sameValue(schoolClass.schoolId, currentSchoolId.value) &&
sameValue(schoolClass.createdBy, currentUserId.value)
)
);
const canOperateStudent = (row) => {
const selectedClass = classMap.value.get(paramsquery.value.schoolClassId);
if (selectedClass && normalizeClassType(selectedClass) === "TEACHING") {
@ -424,10 +469,13 @@ const initPassword = (row, status) => {
.catch(() => {});
}
};
onMounted(() => {
onMounted(async () => {
await userStore.ensureSchoolProductConfig().catch(() => null);
getList();
getClassList();
getFacultyList();
if (!isTeacherRosterMode.value) {
getFacultyList();
}
});
</script>
<style lang="scss" scoped>

@ -30,10 +30,12 @@
type="danger"
plain
size="small"
class="restore-default-button"
aria-label="恢复默认任务,会覆盖当前班级的自定义配置"
:loading="restoring"
@click="restoreAllTasksDefaults"
>
恢复本班全部默认任务
<span>恢复本班全部默认任务</span>
</el-button>
<el-button v-if="selectedTeachingClass" type="primary" size="small" @click="openStudentDemo">
学生端讲解
@ -203,10 +205,12 @@
v-if="!isReadonly"
type="danger"
plain
class="restore-default-button"
aria-label="恢复此任务默认内容,会覆盖当前任务的自定义配置"
:loading="restoring"
@click="restoreTaskDefault"
>
恢复此任务默认内容
<span>恢复此任务默认内容</span>
</el-button>
<el-button v-if="!isReadonly" type="primary" :loading="saving" @click="submitTask"></el-button>
</footer>
@ -753,6 +757,17 @@ function buildPayload() {
}
}
:deep(.restore-default-button.el-button) {
color: #ffffff !important;
border: 0 !important;
background: linear-gradient(180deg, #ec606b, #b72e3d) !important;
&:hover:not(.is-disabled) {
color: #ffffff !important;
background: linear-gradient(180deg, #f97b85, #ca3b4b) !important;
}
}
.tool-link {
height: 28px;
padding: 0 8px;

@ -0,0 +1,19 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const pagePath = path.join(__dirname, "..", "src", "views", "teacherEnd", "class", "index.vue");
const source = fs.readFileSync(pagePath, "utf8");
assert.match(
source,
/<el-table-column\s+v-if="facultyManagementEnabled"\s+label="所属院系"/,
"禁用院系管理后,班级表格不应显示所属院系列"
);
assert.match(
source,
/<el-table-column\s+v-if="majorManagementEnabled"\s+label="所属专业"/,
"禁用专业管理后,班级表格不应显示所属专业列"
);
console.log("class table product config static checks passed");

@ -0,0 +1,13 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const appMainPath = path.join(__dirname, "..", "src", "layout", "components", "AppMain.vue");
const source = fs.readFileSync(appMainPath, "utf8");
assert(
/\.el-table\s*\{[\s\S]*--el-fill-color-lighter:\s*rgba\(2, 16, 27, 0\.72\)/.test(source),
"深色主题必须覆盖 Element Plus 表格 stripe 使用的 --el-fill-color-lighter"
);
console.log("dark table stripe static checks passed");

@ -0,0 +1,21 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const stylePath = path.join(__dirname, "..", "src", "assets", "styles", "element-ui.scss");
const source = fs.readFileSync(stylePath, "utf8");
assert(
/(^|\n)\.el-loading-mask\s*\{[\s\S]*background:\s*rgba\(4, 18, 29, 0\.78\)\s*!important;/m.test(source),
"全局加载遮罩必须使用深色半透明背景,不能回退为 Element Plus 默认白色"
);
assert(
/(^|\n)\.el-loading-spinner\s+\.path\s*\{[\s\S]*stroke:\s*#20d6ff;/m.test(source),
"全局加载动画必须使用深色主题的青色"
);
assert(
/(^|\n)\.el-loading-text\s*\{[\s\S]*color:\s*#d7f8ff;/m.test(source),
"全局加载文案必须在深色遮罩中清晰可见"
);
console.log("global loading mask static checks passed");

@ -0,0 +1,12 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const login = fs.readFileSync(path.resolve(__dirname, "../src/views/login.vue"), "utf8");
assert(
/:deep\(\.el-input__inner::placeholder\),\s*:deep\(input::placeholder\)\s*\{[\s\S]*?color:\s*#b8c0cc;[\s\S]*?-webkit-text-fill-color:\s*#b8c0cc;/.test(login),
"username and password placeholders must use one shared muted color rule"
);
console.log("Login input placeholder color is consistent.");

@ -8,16 +8,22 @@ const router = fs.readFileSync(path.join(root, "src/router/index.js"), "utf8");
const sidebar = fs.readFileSync(path.join(root, "src/layout/components/Sidebar/index.vue"), "utf8");
const platformSchool = fs.readFileSync(path.join(root, "src/views/platformAdmin/school/index.vue"), "utf8");
const teacherClass = fs.readFileSync(path.join(root, "src/views/teacherEnd/class/index.vue"), "utf8");
const userStore = fs.readFileSync(path.join(root, "src/store/modules/user.js"), "utf8");
const teacherRoutes = router.slice(router.indexOf("export const teacherRoutes"), router.indexOf("export const platformAdminRoutes"));
const teacherManagementRoute = teacherRoutes.slice(teacherRoutes.indexOf('path: "/teacher"'), teacherRoutes.indexOf('path: "/student"'));
assert(/\/api\/school-product-config\/current/.test(api), "school users must load their product configuration");
assert(/\/product-config`/.test(api), "platform admins must update a school's product configuration");
assert(/requiresAdminClass:\s*true/.test(router), "administrative organization routes must be marked for configuration gating");
assert(/path: "\/school"[\s\S]*?requiresAdminClass:\s*true/.test(teacherRoutes), "teacher faculty menu must be gated");
assert(/path: "\/major"[\s\S]*?requiresAdminClass:\s*true/.test(teacherRoutes), "teacher major menu must be gated");
assert(/path: "\/teacher"[\s\S]*?requiresAdminClass:\s*true/.test(teacherRoutes), "teacher account menu must be gated");
assert(/path: "\/school"[\s\S]*?requiresFacultyManagement:\s*true/.test(teacherRoutes), "teacher faculty menu must be gated by the faculty switch");
assert(/path: "\/major"[\s\S]*?requiresMajorManagement:\s*true/.test(teacherRoutes), "teacher major menu must be gated by the major switch");
assert(!/requiresAdminClass/.test(teacherManagementRoute), "teacher management must remain available when administrative-class management is disabled");
assert(/filterRoutesByOrganizationMode/.test(sidebar), "sidebar must remove unavailable administrative organization routes");
assert(/filterRoutesByOrganizationMode\(teacherRoutes\)/.test(sidebar), "teacher menus must be filtered by organization mode");
assert(/requiresFacultyManagement:\s*true/.test(sidebar), "the teacher course-style faculty item must declare its capability");
assert(/requiresMajorManagement:\s*true/.test(sidebar), "the teacher course-style major item must declare its capability");
assert(/filterTeacherManageNavigation\(teacherManageNav\)/.test(sidebar), "the teacher course-style sidebar must filter its hard-coded navigation before rendering");
assert(/currentCourseNav = computed\([\s\S]*?filterTeacherManageNavigation\(teacherManageNav\)/.test(sidebar), "teachers must render the filtered course-style navigation instead of the raw hard-coded menu");
assert(/watch\(\s*\(\) => userStore\.schoolProductConfig\?\.organizationMode/.test(sidebar), "sidebar must refresh when the asynchronous school configuration arrives");
assert(/organizationMode/.test(platformSchool) && /updatePlatformSchoolProductConfig/.test(platformSchool), "platform school editor must manage organization mode");
assert(/行政班管理/.test(platformSchool), "platform school editor must expose administrative-class management controls");
@ -27,5 +33,14 @@ assert(/v-model="productConfig\.adminClassManagementEnabled"/.test(platformSchoo
assert(/requiresFacultyManagement/.test(router) && /requiresMajorManagement/.test(router), "teacher routes must declare their management capability");
assert(/adminClassManagementEnabled/.test(teacherClass), "teacher class page must derive administrative-class controls from the switch");
assert(/isTeacherRosterMode/.test(teacherClass), "teacher class page must recognize teacher-managed roster mode");
assert(!/!this\.userInfo\?\.schoolId\s*\|\|\s*this\.userInfo\?\.roleId\s*==\s*1/.test(userStore), "school configuration loading must not be skipped because cached user info lacks schoolId");
assert(/this\.userInfo\?\.roleId\s*==\s*1/.test(userStore) && /return getCurrentSchoolProductConfig\(\)/.test(userStore), "every non-platform user must load school configuration from the backend token");
assert(/await userStore\.ensureSchoolProductConfig\(\)/.test(sidebar), "school menus must load the product configuration before they are generated");
assert(/const facultyEnabled = config\?\.facultyManagementEnabled === true;/.test(sidebar), "faculty menus must fail closed until their school capability is confirmed");
assert(/const majorEnabled = config\?\.majorManagementEnabled === true;/.test(sidebar), "major menus must fail closed until their school capability is confirmed");
assert(/visibleSidebarRouters/.test(sidebar), "sidebar rendering must apply school capability filtering as a final guard");
assert(/v-for="\(route, index\) in visibleSidebarRouters"/.test(sidebar), "the sidebar must render the final filtered routes instead of the raw route list");
assert(/:key="sidebarMenuKey"/.test(sidebar), "the Element Plus menu must remount when visible school routes change");
assert(/const sidebarMenuKey = computed\(\(\) => visibleSidebarRouters\.value\.map\(\(route\) => route\.path\)\.join\("\|"\)\);/.test(sidebar), "menu remount key must derive from the visible routes");
console.log("School product configuration is represented in the API, routes, sidebar, and platform editor.");

@ -10,6 +10,8 @@ const demoStorage = read("src/utils/studentDemo.js");
const api = read("src/api/studentDemo.js");
const teacherPage = read("src/views/teacherEnd/trainingTask/index.vue");
const demoPage = read("src/views/studentDemo/index.vue");
const navbar = read("src/layout/components/Navbar.vue");
const appMain = read("src/layout/components/AppMain.vue");
assert(auth.includes("getStudentDemoSession") && demoStorage.includes("sessionStorage"), "demo token must use tab-local session storage");
assert(api.includes("/api/teacher/student-demo-sessions"), "teacher ticket API missing");
@ -20,5 +22,8 @@ assert(teacherPage.includes("STUDENT_DEMO_READY"), "teacher must wait for child
assert(demoPage.includes("STUDENT_DEMO_TICKET"), "demo page must receive ticket via postMessage");
assert(demoPage.includes("window.location.origin"), "postMessage must validate origin");
assert(!teacherPage.includes("ticket="), "ticket must not be sent in URL");
assert(navbar.includes("isStudentDemo()"), "demo mode must not request a profile for its temporary user");
assert(appMain.includes("window.close()"), "exiting a demo must close the demo window");
assert(appMain.includes('window.location.replace("/index")'), "blocked close must return to the application home page");
console.log("student demo mode front-end contract passed");

@ -0,0 +1,31 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const pagePath = path.join(__dirname, "..", "src", "views", "teacherEnd", "student", "index.vue");
const apiPath = path.join(__dirname, "..", "src", "api", "teacher.js");
const page = fs.readFileSync(pagePath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
assert(
page.includes('const isTeacherRosterMode = computed(() => userStore.schoolProductConfig?.studentRosterOwner === "TEACHER")'),
"教师端学生页必须根据学校配置识别教师维护名单模式"
);
assert(page.includes(':rules="studentFormRules"'), "教师维护名单模式不能继续校验院系、专业、班级");
assert(page.includes('v-if="isTeacherRosterMode" label="所属教学班:" prop="teachingClassId"'), "教师维护名单模式新增和编辑学生时必须选择教学班");
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 的组织结构接口"
);
assert(page.includes("indexApi.addTeacherRosterStudent(formInline.value)"), "教师维护名单模式新增学生必须使用无组织字段的名单接口");
assert(page.includes("indexApi.updateTeacherRosterStudent(formInline.value)"), "教师维护名单模式编辑学生必须使用无班级归属的名单接口");
assert(
page.includes("row.teachingClassId || currentTeachingClassId()"),
"教师维护名单模式再次编辑学生时必须回显该学生当前所属教学班"
);
assert(api.includes('url: "/api/user/teacher-roster/students"'), "教师名单新增接口必须在前端 API 层声明");
assert(api.includes('teachingClassId: data.teachingClassId'), "教师名单接口必须提交已选择的教学班");
console.log("teacher student roster mode static checks passed");

@ -16,5 +16,8 @@ assert(/恢复此任务默认内容/.test(page));
assert(/恢复本班全部默认任务/.test(page));
assert(/proxy\?\.\$modal\?\.confirm/.test(page));
assert(/v-if="selectedTeachingClass"/.test(page));
assert(/class="restore-default-button"/.test(page));
assert(/aria-label="恢复默认任务,会覆盖当前班级的自定义配置"/.test(page));
assert(/\.restore-default-button\.el-button[\s\S]*linear-gradient\(180deg, #ec606b, #b72e3d\)/.test(page));
console.log("training task default restore front-end contract passed");

Loading…
Cancel
Save