feat: gate school administration by organization mode

dev-QQq
chenyuan 1 month ago
parent 81b05bf011
commit 9afd17cc68

@ -0,0 +1,23 @@
import request from "@/utils/request";
export function getCurrentSchoolProductConfig() {
return request({
url: "/api/school-product-config/current",
method: "get",
});
}
export function getPlatformSchoolProductConfig(schoolId) {
return request({
url: `/api/platform-admin/schools/${schoolId}/product-config`,
method: "get",
});
}
export function updatePlatformSchoolProductConfig(schoolId, data) {
return request({
url: `/api/platform-admin/schools/${schoolId}/product-config`,
method: "put",
data,
});
}

@ -565,7 +565,7 @@ const getTaskList = () => {
if (userStore.userInfo.roleId == 1) {
sidebarRouters.value = [...constantRoutes, ...platformAdminRoutes];
} else if (userStore.userInfo.roleId == 2) {
sidebarRouters.value = [...constantRoutes, ...schoolAdminRoutes];
sidebarRouters.value = [...constantRoutes, ...filterRoutesByOrganizationMode(schoolAdminRoutes)];
} else if (userStore.userInfo.roleId != 3) {
indexApi.getStudentTaskList({ userId: userStore.userInfo.userId }).then((res) => {
const disabledTitles = processModuleStatus(groupAndSortModules(res.data)).disabledParents;
@ -577,7 +577,14 @@ const getTaskList = () => {
} else {
sidebarRouters.value = [...constantRoutes, ...teacherRoutes];
}
};
};
function filterRoutesByOrganizationMode(routes) {
if (userStore.schoolProductConfig?.organizationMode !== "TEACHING_CLASS_ONLY") {
return routes;
}
return routes.filter((route) => !route.meta?.requiresAdminClass);
}
onMounted(() => {
getTaskList();
loadTrainingTaskTitles();

@ -38,9 +38,19 @@ router.beforeEach((to, from, next) => {
NProgress.done();
}
} else {
usePermissionStore()
.getRoutersPermission()
.then((accessRoutes) => {
const userStore = useUserStore();
userStore
.ensureSchoolProductConfig()
.then((config) => {
if (config?.organizationMode === "TEACHING_CLASS_ONLY" && to.matched.some((record) => record.meta?.requiresAdminClass)) {
next({ path: "/admin-school/index" });
NProgress.done();
return null;
}
return usePermissionStore().getRoutersPermission();
})
.then((accessRoutes) => {
if (accessRoutes === null) return;
next(); // hack方法 确保addRoutes已完成
});
}

@ -653,7 +653,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-faculty/index",
roles: ["schoolAdmin"],
meta: { title: "院系管理", icon: "院系管理", affix: true },
meta: { title: "院系管理", icon: "院系管理", affix: true, requiresAdminClass: true },
children: [
{
path: "index",
@ -668,7 +668,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-major/index",
roles: ["schoolAdmin"],
meta: { title: "专业管理", icon: "专业管理", affix: true },
meta: { title: "专业管理", icon: "专业管理", affix: true, requiresAdminClass: true },
children: [
{
path: "index",
@ -683,7 +683,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-class/index",
roles: ["schoolAdmin"],
meta: { title: "班级管理", icon: "班级管理", affix: true },
meta: { title: "班级管理", icon: "班级管理", affix: true, requiresAdminClass: true },
children: [
{
path: "index",
@ -713,7 +713,7 @@ export const schoolAdminRoutes = [
component: Layout,
redirect: "/admin-student/index",
roles: ["schoolAdmin"],
meta: { title: "学生管理", icon: "学生管理", affix: true },
meta: { title: "学生管理", icon: "学生管理", affix: true, requiresAdminClass: true },
children: [
{
path: "index",

@ -6,7 +6,8 @@
* @FilePath: \vue3\src\store\modules\user.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { login, logout, getInfo } from "@/api/login";
import { login, logout, getInfo } from "@/api/login";
import { getCurrentSchoolProductConfig } from "@/api/schoolProductConfig";
import { getToken, setToken, removeToken, setUserInfo, removeUserInfo, getUserInfo } from "@/utils/auth";
import defAva from "@/assets/images/profile.jpg";
const useUserStore = defineStore("user", {
@ -16,8 +17,9 @@ const useUserStore = defineStore("user", {
avatar: "",
roles: ["teacher"],
permissions: [],
userInfo: JSON.parse(getUserInfo() ?? "{}"),
classList: [],
userInfo: JSON.parse(getUserInfo() ?? "{}"),
classList: [],
schoolProductConfig: null,
}),
actions: {
// 登录
@ -40,7 +42,7 @@ const useUserStore = defineStore("user", {
});
},
// 获取用户信息
getInfo() {
getInfo() {
return new Promise((resolve, reject) => {
getInfo()
.then((res) => {
@ -61,14 +63,27 @@ const useUserStore = defineStore("user", {
reject(error);
});
});
},
},
ensureSchoolProductConfig() {
if (this.schoolProductConfig) {
return Promise.resolve(this.schoolProductConfig);
}
if (!this.userInfo?.schoolId || this.userInfo?.roleId == 1) {
return Promise.resolve(null);
}
return getCurrentSchoolProductConfig().then((res) => {
this.schoolProductConfig = res.data;
return this.schoolProductConfig;
});
},
// 退出系统
logOut() {
return new Promise((resolve, reject) => {
// logout(this.token).then(() => {
this.token = "";
this.roles = [];
this.permissions = [];
this.permissions = [];
this.schoolProductConfig = null;
removeToken();
removeUserInfo();
resolve();

@ -77,6 +77,13 @@
<el-form-item label="学校Logo">
<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-option label="标准学校:行政班 + 教学班" value="ADMIN_CLASS_ENABLED" />
<el-option label="教师教学班维护名单" value="TEACHING_CLASS_ONLY" />
</el-select>
<div class="config-hint">新建学校默认采用标准模式已有行政组织数据的学校不能直接切换模式</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@ -91,6 +98,7 @@
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addPlatformSchool, deletePlatformSchool, getPlatformSchools, updatePlatformSchool } from "@/api/platformAdmin";
import { getPlatformSchoolProductConfig, updatePlatformSchoolProductConfig } from "@/api/schoolProductConfig";
import { chinaRegions } from "@/utils/chinaRegions";
const query = reactive({ index: 1, size: 10, schoolName: "" });
@ -100,6 +108,7 @@ 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 cityOptions = computed(() => chinaRegions.find((item) => item.province === form.value.schoolProvince)?.cities || []);
const load = async (options = {}) => {
@ -141,12 +150,15 @@ const openCreate = () => {
schoolContactsEmail: "",
schoolLogo: "",
};
productConfig.value = { organizationMode: "ADMIN_CLASS_ENABLED", studentRosterOwner: "SCHOOL_ADMIN" };
dialogVisible.value = true;
};
const openEdit = (row) => {
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" };
dialogVisible.value = true;
};
@ -168,6 +180,11 @@ 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,
});
await updatePlatformSchool(editingId.value, payload);
} else {
await addPlatformSchool(payload);
@ -205,6 +222,13 @@ onMounted(load);
height: 96px;
}
.config-hint {
color: #7a8499;
font-size: 12px;
line-height: 1.6;
margin-top: 6px;
}
@media (max-width: 640px) {
.form-inline {
grid-template-columns: 1fr;

@ -0,0 +1,17 @@
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const root = path.resolve(__dirname, "..");
const api = fs.readFileSync(path.join(root, "src/api/schoolProductConfig.js"), "utf8");
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");
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(/filterRoutesByOrganizationMode/.test(sidebar), "sidebar must remove unavailable administrative organization routes");
assert(/organizationMode/.test(platformSchool) && /updatePlatformSchoolProductConfig/.test(platformSchool), "platform school editor must manage organization mode");
console.log("School product configuration is represented in the API, routes, sidebar, and platform editor.");
Loading…
Cancel
Save