feat: configure school organization menus

dev-QQq
chenyuan 1 month ago
parent 9afd17cc68
commit c4fd23ad54

@ -575,16 +575,32 @@ const getTaskList = () => {
sidebarRouters.value = [...constantRoutes, ...dynamicRoutes];
});
} else {
sidebarRouters.value = [...constantRoutes, ...teacherRoutes];
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(teacherRoutes)];
}
};
function filterRoutesByOrganizationMode(routes) {
if (userStore.schoolProductConfig?.organizationMode !== "TEACHING_CLASS_ONLY") {
return routes;
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?.requiresFacultyManagement && !facultyEnabled) return false;
if (route.meta?.requiresMajorManagement && !majorEnabled) return false;
if (route.meta?.requiresAdminClass && !adminClassEnabled) return false;
return true;
});
}
watch(
() => userStore.schoolProductConfig?.organizationMode,
(organizationMode, previousOrganizationMode) => {
if (organizationMode === previousOrganizationMode) return;
if ([2, 3].includes(Number(userStore.userInfo.roleId))) {
getTaskList();
}
return routes.filter((route) => !route.meta?.requiresAdminClass);
}
);
onMounted(() => {
getTaskList();
loadTrainingTaskTitles();

@ -42,7 +42,16 @@ router.beforeEach((to, from, next) => {
userStore
.ensureSchoolProductConfig()
.then((config) => {
if (config?.organizationMode === "TEACHING_CLASS_ONLY" && to.matched.some((record) => record.meta?.requiresAdminClass)) {
const legacyAdminEnabled = config?.organizationMode !== "TEACHING_CLASS_ONLY";
const facultyEnabled = config?.facultyManagementEnabled ?? legacyAdminEnabled;
const majorEnabled = config?.majorManagementEnabled ?? legacyAdminEnabled;
const adminClassEnabled = config?.adminClassManagementEnabled ?? legacyAdminEnabled;
const unavailableManagementRoute = to.matched.some((record) =>
(record.meta?.requiresFacultyManagement && !facultyEnabled)
|| (record.meta?.requiresMajorManagement && !majorEnabled)
|| (record.meta?.requiresAdminClass && !adminClassEnabled)
);
if (unavailableManagementRoute) {
next({ path: "/admin-school/index" });
NProgress.done();
return null;

@ -455,7 +455,7 @@ export const teacherRoutes = [
component: Layout,
redirect: "/school/index",
roles: ["teacher"],
meta: { title: "院系管系", icon: "院系管系", affix: true },
meta: { title: "院系管系", icon: "院系管系", affix: true, requiresFacultyManagement: true },
children: [
// 院校管理
{
@ -472,7 +472,7 @@ export const teacherRoutes = [
component: Layout,
redirect: "/major/index",
roles: ["teacher"],
meta: { title: "专业管理", icon: "专业管理", affix: true },
meta: { title: "专业管理", icon: "专业管理", affix: true, requiresMajorManagement: true },
children: [
// 专业管理
{
@ -506,7 +506,7 @@ export const teacherRoutes = [
component: Layout,
redirect: "/teacher/index",
roles: ["teacher"],
meta: { title: "教师管理", icon: "教师管理", affix: true },
meta: { title: "教师管理", icon: "教师管理", affix: true, requiresAdminClass: true },
children: [
// 教师管理
{
@ -653,7 +653,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-faculty/index",
roles: ["schoolAdmin"],
meta: { title: "院系管理", icon: "院系管理", affix: true, requiresAdminClass: true },
meta: { title: "院系管理", icon: "院系管理", affix: true, requiresFacultyManagement: true },
children: [
{
path: "index",
@ -668,7 +668,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-major/index",
roles: ["schoolAdmin"],
meta: { title: "专业管理", icon: "专业管理", affix: true, requiresAdminClass: true },
meta: { title: "专业管理", icon: "专业管理", affix: true, requiresMajorManagement: true },
children: [
{
path: "index",

@ -78,11 +78,24 @@
<ImageUpload v-model="form.schoolLogo" class="school-logo-upload" :limit="1" :file-size="2" />
</el-form-item>
<el-form-item label="组织模式">
<el-select v-model="productConfig.organizationMode" :disabled="!editingId" style="width: 100%">
<el-select v-model="productConfig.organizationMode" :disabled="!editingId" style="width: 100%" @change="applyOrganizationPreset">
<el-option label="标准学校:行政班 + 教学班" value="ADMIN_CLASS_ENABLED" />
<el-option label="教师教学班维护名单" value="TEACHING_CLASS_ONLY" />
<el-option label="自定义配置(由下方开关形成)" value="CUSTOM" disabled />
</el-select>
<div class="config-hint">新建学校默认采用标准模式已有行政组织数据的学校不能直接切换模式</div>
<div class="config-hint">选择预设会同步更新下方开关已有组织数据会保留关闭开关仅隐藏菜单和阻止新增编辑</div>
</el-form-item>
<el-form-item label="院系管理">
<el-switch v-model="productConfig.facultyManagementEnabled" :disabled="!editingId" @change="handleFacultyManagementChange" />
<span class="config-hint">关闭后会同时关闭专业管理和行政班管理</span>
</el-form-item>
<el-form-item label="专业管理">
<el-switch v-model="productConfig.majorManagementEnabled" :disabled="!editingId || !productConfig.facultyManagementEnabled" @change="handleMajorManagementChange" />
<span class="config-hint">需先启用院系管理关闭后会同时关闭行政班管理</span>
</el-form-item>
<el-form-item label="行政班管理">
<el-switch v-model="productConfig.adminClassManagementEnabled" :disabled="!editingId || !productConfig.facultyManagementEnabled || !productConfig.majorManagementEnabled" @change="syncOrganizationModeFromSwitches" />
<span class="config-hint">关闭后班级管理仅保留教学班教师维护学生名单</span>
</el-form-item>
</el-form>
<template #footer>
@ -108,7 +121,14 @@ const loading = ref(false);
const dialogVisible = ref(false);
const editingId = ref("");
const form = ref({});
const productConfig = ref({ organizationMode: "ADMIN_CLASS_ENABLED", studentRosterOwner: "SCHOOL_ADMIN" });
const defaultProductConfig = () => ({
organizationMode: "ADMIN_CLASS_ENABLED",
studentRosterOwner: "SCHOOL_ADMIN",
facultyManagementEnabled: true,
majorManagementEnabled: true,
adminClassManagementEnabled: true,
});
const productConfig = ref(defaultProductConfig());
const cityOptions = computed(() => chinaRegions.find((item) => item.province === form.value.schoolProvince)?.cities || []);
const load = async (options = {}) => {
@ -150,7 +170,7 @@ const openCreate = () => {
schoolContactsEmail: "",
schoolLogo: "",
};
productConfig.value = { organizationMode: "ADMIN_CLASS_ENABLED", studentRosterOwner: "SCHOOL_ADMIN" };
productConfig.value = defaultProductConfig();
dialogVisible.value = true;
};
@ -158,7 +178,7 @@ const openEdit = async (row) => {
editingId.value = row.schoolId;
form.value = { ...row };
const res = await getPlatformSchoolProductConfig(row.schoolId);
productConfig.value = res.data || { organizationMode: "ADMIN_CLASS_ENABLED", studentRosterOwner: "SCHOOL_ADMIN" };
productConfig.value = normalizeProductConfig(res.data);
dialogVisible.value = true;
};
@ -166,6 +186,63 @@ const handleProvinceChange = () => {
form.value.schoolCity = "";
};
const normalizeProductConfig = (config) => {
const current = config || defaultProductConfig();
const legacyAdminEnabled = current.organizationMode !== "TEACHING_CLASS_ONLY";
return {
...current,
facultyManagementEnabled: current.facultyManagementEnabled ?? legacyAdminEnabled,
majorManagementEnabled: current.majorManagementEnabled ?? legacyAdminEnabled,
adminClassManagementEnabled: current.adminClassManagementEnabled ?? legacyAdminEnabled,
};
};
const applyOrganizationPreset = (organizationMode) => {
if (organizationMode === "ADMIN_CLASS_ENABLED") {
productConfig.value = { ...productConfig.value, ...defaultProductConfig() };
return;
}
if (organizationMode === "TEACHING_CLASS_ONLY") {
productConfig.value = {
...productConfig.value,
organizationMode: "TEACHING_CLASS_ONLY",
studentRosterOwner: "TEACHER",
facultyManagementEnabled: false,
majorManagementEnabled: false,
adminClassManagementEnabled: false,
};
}
};
const handleFacultyManagementChange = (enabled) => {
if (!enabled) {
productConfig.value.majorManagementEnabled = false;
productConfig.value.adminClassManagementEnabled = false;
}
syncOrganizationModeFromSwitches();
};
const handleMajorManagementChange = (enabled) => {
if (!enabled) {
productConfig.value.adminClassManagementEnabled = false;
}
syncOrganizationModeFromSwitches();
};
const syncOrganizationModeFromSwitches = () => {
const { facultyManagementEnabled, majorManagementEnabled, adminClassManagementEnabled } = productConfig.value;
if (facultyManagementEnabled && majorManagementEnabled && adminClassManagementEnabled) {
productConfig.value.organizationMode = "ADMIN_CLASS_ENABLED";
productConfig.value.studentRosterOwner = "SCHOOL_ADMIN";
} else if (!facultyManagementEnabled && !majorManagementEnabled && !adminClassManagementEnabled) {
productConfig.value.organizationMode = "TEACHING_CLASS_ONLY";
productConfig.value.studentRosterOwner = "TEACHER";
} else {
productConfig.value.organizationMode = "CUSTOM";
productConfig.value.studentRosterOwner = "TEACHER";
}
};
const buildSchoolPayload = () => ({
schoolName: form.value.schoolName,
schoolGrade: form.value.schoolGrade,
@ -180,10 +257,12 @@ const buildSchoolPayload = () => ({
const submit = async () => {
const payload = buildSchoolPayload();
if (editingId.value) {
const studentRosterOwner = productConfig.value.organizationMode === "TEACHING_CLASS_ONLY" ? "TEACHER" : "SCHOOL_ADMIN";
await updatePlatformSchoolProductConfig(editingId.value, {
organizationMode: productConfig.value.organizationMode,
studentRosterOwner,
studentRosterOwner: productConfig.value.studentRosterOwner,
facultyManagementEnabled: productConfig.value.facultyManagementEnabled,
majorManagementEnabled: productConfig.value.majorManagementEnabled,
adminClassManagementEnabled: productConfig.value.adminClassManagementEnabled,
});
await updatePlatformSchool(editingId.value, payload);
} else {

@ -1,15 +1,15 @@
<template>
<div class="class-page app-container">
<el-tabs v-model="activeClassType" class="class-tabs" @tab-change="handleTabChange">
<el-tabs v-if="adminClassManagementEnabled" v-model="activeClassType" class="class-tabs" @tab-change="handleTabChange">
<el-tab-pane label="行政班" name="ADMIN" />
<el-tab-pane label="教学班" name="TEACHING" />
</el-tabs>
<div class="query-bar">
<el-select v-model="paramsquery.schoolFacultyId" placeholder="所属院系" clearable class="query-control" @change="getMajorList">
<el-select v-if="facultyManagementEnabled" v-model="paramsquery.schoolFacultyId" placeholder="所属院系" clearable class="query-control" @change="getMajorList">
<el-option v-for="faculty in facultyList" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
<el-select v-model="paramsquery.schoolMajorId" placeholder="所属专业" clearable class="query-control">
<el-select v-if="majorManagementEnabled" v-model="paramsquery.schoolMajorId" placeholder="所属专业" clearable class="query-control">
<el-option v-for="major in majorList" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
<el-input v-model="paramsquery.className" placeholder="班级名称" clearable class="query-control" />
@ -73,17 +73,17 @@
<el-dialog v-model="dialogVisible" width="520px" :title="dialogTitle" class="teacher-dialog" @close="onClose">
<el-form ref="formInlineref" :model="formInline" label-width="96px" :rules="rules">
<el-form-item label="所属院系" prop="schoolFacultyId">
<el-form-item v-if="facultyManagementEnabled" label="所属院系" prop="schoolFacultyId">
<el-select v-model="formInline.schoolFacultyId" placeholder="请选择院系" clearable class="form-control" @change="getMajorListForForm">
<el-option v-for="faculty in facultyList" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
</el-form-item>
<el-form-item label="所属专业" prop="schoolMajorId">
<el-form-item v-if="majorManagementEnabled" label="所属专业" prop="schoolMajorId">
<el-select v-model="formInline.schoolMajorId" placeholder="请选择专业" clearable class="form-control" @change="handleFormMajorChange">
<el-option v-for="major in majorListForForm" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
</el-form-item>
<el-form-item v-if="activeClassType === 'TEACHING' && isCreate" label="来源行政班" prop="sourceClassIds">
<el-form-item v-if="activeClassType === 'TEACHING' && isCreate && adminClassManagementEnabled" label="来源行政班" prop="sourceClassIds">
<el-select v-model="sourceAdminClassIds" placeholder="请选择来源行政班" clearable multiple collapse-tags collapse-tags-tooltip class="form-control">
<el-option v-for="schoolClass in adminClassOptions" :key="schoolClass.schoolClassId" :label="schoolClass.className" :value="schoolClass.schoolClassId" />
</el-select>
@ -190,6 +190,11 @@ const selectedTeachingClass = ref(null);
const currentUserId = computed(() => userStore.userInfo.userId);
const currentSchoolId = computed(() => userStore.userInfo.schoolId);
const legacyAdminEnabled = computed(() => userStore.schoolProductConfig?.organizationMode !== "TEACHING_CLASS_ONLY");
const facultyManagementEnabled = computed(() => userStore.schoolProductConfig?.facultyManagementEnabled ?? legacyAdminEnabled.value);
const majorManagementEnabled = computed(() => userStore.schoolProductConfig?.majorManagementEnabled ?? legacyAdminEnabled.value);
const adminClassManagementEnabled = computed(() => userStore.schoolProductConfig?.adminClassManagementEnabled ?? legacyAdminEnabled.value);
const isTeacherRosterMode = computed(() => !facultyManagementEnabled.value && !majorManagementEnabled.value && !adminClassManagementEnabled.value);
const canCreateCurrentClass = computed(() => activeClassType.value === "ADMIN" || activeClassType.value === "TEACHING");
const paramsquery = ref({
@ -204,11 +209,11 @@ const paramsquery = ref({
const formInline = ref(defaultForm());
const memberParams = ref(defaultMemberParams());
const rules = {
schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }],
schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }],
const rules = computed(() => ({
...(facultyManagementEnabled.value ? { schoolFacultyId: [{ required: true, message: "请选择所属院系", trigger: "change" }] } : {}),
...(majorManagementEnabled.value ? { schoolMajorId: [{ required: true, message: "请选择所属专业", trigger: "change" }] } : {}),
className: [{ required: true, message: "请输入班级名称", trigger: "blur" }],
};
}));
const tableHeaderStyle = {
color: "#0f3052",
@ -403,7 +408,7 @@ function updateClass() {
function submitForm() {
proxy.$refs.formInlineref.validate((valid) => {
if (!valid) return;
if (isCreate.value && activeClassType.value === "TEACHING" && sourceAdminClassIds.value.length === 0) {
if (adminClassManagementEnabled.value && isCreate.value && activeClassType.value === "TEACHING" && sourceAdminClassIds.value.length === 0) {
proxy.$message.warning("请选择来源行政班");
return;
}
@ -767,6 +772,9 @@ function refreshAllClassList() {
}
onMounted(() => {
if (!adminClassManagementEnabled.value) {
activeClassType.value = "TEACHING";
}
getList();
getAllClassList();
getFacultyList();

@ -7,11 +7,25 @@ const api = fs.readFileSync(path.join(root, "src/api/schoolProductConfig.js"), "
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 teacherRoutes = router.slice(router.indexOf("export const teacherRoutes"), router.indexOf("export const platformAdminRoutes"));
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(/filterRoutesByOrganizationMode/.test(sidebar), "sidebar must remove unavailable administrative organization routes");
assert(/filterRoutesByOrganizationMode\(teacherRoutes\)/.test(sidebar), "teacher menus must be filtered by organization mode");
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");
assert(/v-model="productConfig\.facultyManagementEnabled"/.test(platformSchool), "platform school editor must configure faculty management with a switch");
assert(/v-model="productConfig\.majorManagementEnabled"/.test(platformSchool), "platform school editor must configure major management with a switch");
assert(/v-model="productConfig\.adminClassManagementEnabled"/.test(platformSchool), "platform school editor must configure administrative classes with a switch");
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");
console.log("School product configuration is represented in the API, routes, sidebar, and platform editor.");

Loading…
Cancel
Save