feat: add platform school admin management

fix/non-blocking-route-navigation
chenyuan 4 months ago
parent 3f767d6330
commit 9ba591e7a1

@ -0,0 +1,77 @@
import request from "@/utils/request";
export function getPlatformSchools(params) {
return request({
url: "/api/platform-admin/schools",
method: "get",
params,
});
}
export function getPlatformSchool(id) {
return request({
url: `/api/platform-admin/schools/${id}`,
method: "get",
});
}
export function addPlatformSchool(data) {
return request({
url: "/api/platform-admin/schools",
method: "post",
data,
});
}
export function updatePlatformSchool(id, data) {
return request({
url: `/api/platform-admin/schools/${id}`,
method: "put",
data,
});
}
export function deletePlatformSchool(id) {
return request({
url: `/api/platform-admin/schools/${id}`,
method: "delete",
});
}
export function getPlatformSchoolAdmins(params) {
return request({
url: "/api/platform-admin/school-admins",
method: "get",
params,
});
}
export function getPlatformSchoolAdmin(id) {
return request({
url: `/api/platform-admin/school-admins/${id}`,
method: "get",
});
}
export function addPlatformSchoolAdmin(data) {
return request({
url: "/api/platform-admin/school-admins",
method: "post",
data,
});
}
export function updatePlatformSchoolAdmin(id, data) {
return request({
url: `/api/platform-admin/school-admins/${id}`,
method: "put",
data,
});
}
export function deletePlatformSchoolAdmin(id) {
return request({
url: `/api/platform-admin/school-admins/${id}`,
method: "delete",
});
}

@ -0,0 +1,171 @@
import request from "@/utils/request";
export function getAdminSchool() {
return request({
url: "/api/school-admin/school",
method: "get",
});
}
export function updateAdminSchool(data) {
return request({
url: "/api/school-admin/school",
method: "put",
data,
});
}
export function getAdminFaculties(params) {
return request({
url: "/api/school-admin/faculties",
method: "get",
params,
});
}
export function addAdminFaculty(data) {
return request({
url: "/api/school-admin/faculties",
method: "post",
data,
});
}
export function updateAdminFaculty(id, data) {
return request({
url: `/api/school-admin/faculties/${id}`,
method: "put",
data,
});
}
export function deleteAdminFaculty(id) {
return request({
url: `/api/school-admin/faculties/${id}`,
method: "delete",
});
}
export function getAdminMajors(params) {
return request({
url: "/api/school-admin/majors",
method: "get",
params,
});
}
export function addAdminMajor(data) {
return request({
url: "/api/school-admin/majors",
method: "post",
data,
});
}
export function updateAdminMajor(id, data) {
return request({
url: `/api/school-admin/majors/${id}`,
method: "put",
data,
});
}
export function deleteAdminMajor(id) {
return request({
url: `/api/school-admin/majors/${id}`,
method: "delete",
});
}
export function getAdminClasses(params) {
return request({
url: "/api/school-admin/classes",
method: "get",
params,
});
}
export function addAdminClass(data) {
return request({
url: "/api/school-admin/classes",
method: "post",
data,
});
}
export function updateAdminClass(id, data) {
return request({
url: `/api/school-admin/classes/${id}`,
method: "put",
data,
});
}
export function deleteAdminClass(id) {
return request({
url: `/api/school-admin/classes/${id}`,
method: "delete",
});
}
export function getAdminTeachers(params) {
return request({
url: "/api/school-admin/teachers",
method: "get",
params,
});
}
export function addAdminTeacher(data) {
return request({
url: "/api/school-admin/teachers",
method: "post",
data,
});
}
export function updateAdminTeacher(id, data) {
return request({
url: `/api/school-admin/teachers/${id}`,
method: "put",
data,
});
}
export function deleteAdminTeacher(id) {
return request({
url: `/api/school-admin/teachers/${id}`,
method: "delete",
});
}
export function getAdminStudents(params) {
return request({
url: "/api/school-admin/students",
method: "get",
params,
});
}
export function addAdminStudent(data) {
return request({
url: "/api/school-admin/students",
method: "post",
data,
});
}
export function updateAdminStudent(id, data) {
return request({
url: `/api/school-admin/students/${id}`,
method: "put",
data,
});
}
export function deleteAdminStudent(id) {
return request({
url: `/api/school-admin/students/${id}`,
method: "delete",
});
}

@ -4,11 +4,11 @@
<span class="nacber-name" v-if="role !== 3">电商互联网产品开发实训系统</span>
<span class="nacber-name" v-else>数 字 营 销 实 训 系 统 - 教 师 端</span>
</div>
<div class="el-ment">
<template v-for="item in roleList" :key="item.name">
<div :class="['el-ment-item', { 'el-ment-item-active': item.path == $route.path }]" @click="go(item)">{{ item.name }}</div>
</template>
</div>
<div :class="['el-ment', { 'platform-nav': role === 1 }]">
<template v-for="item in roleList" :key="item.name">
<div :class="['el-ment-item', { 'el-ment-item-active': isActive(item) }]" @click="go(item)">{{ item.name }}</div>
</template>
</div>
<div class="right-menu">
<div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
@ -83,19 +83,32 @@ import useSettingsStore from "@/store/modules/settings";
import * as indexApi from "@/api/teacher";
const { proxy } = getCurrentInstance();
const router = useRouter();
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const showModel = ref(false);
const form = ref({});
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const showModel = ref(false);
const form = ref({});
const role = computed(() => Number(userStore.userInfo.roleId));
const mentList = ref([
{
name: "首页",
path: "/index",
},
{
name: "实验报告",
path: "/report/index",
{
name: "首页",
path: "/index",
},
{
name: "学校管理",
path: "/platform-schools/index",
role: 1,
activePrefix: "/platform-schools",
},
{
name: "学校管理员",
path: "/platform-school-admins/index",
role: 1,
activePrefix: "/platform-school-admins",
},
{
name: "实验报告",
path: "/report/index",
role: 4,
},
{
@ -151,21 +164,43 @@ function logout() {
function setLayout() {
showModel.value = true;
}
const go = (item) => {
activement.value = item;
router.push(item.path);
};
// 根据角色显示菜单
const roleList = computed(() => {
return mentList.value.filter((item) => {
if (!item.role || item.name === "首页" || item.name === "AI学情分析") return true;
return item.role == userStore.userInfo.roleId;
});
});
const go = (item) => {
activement.value = item;
router.push(item.path);
};
const isActive = (item) => {
return item.activePrefix ? router.currentRoute.value.path.startsWith(item.activePrefix) : item.path === router.currentRoute.value.path;
};
// 根据角色显示菜单
const roleList = computed(() => {
const currentRole = role.value;
return mentList.value.filter((item) => {
if (currentRole === 1) {
return item.path === "/index" || item.role === 1;
}
if (currentRole === 2) {
return item.path === "/index";
}
if (!item.role || item.path === "/index" || item.path === "/analysis/index") return true;
return item.role == currentRole;
});
});
// 获取用户详情
const getUserInfo = () => {
indexApi
.getInfo({ userId: userStore.userInfo.userId })
const getUserInfo = () => {
if (role.value === 1) {
form.value = {
username: userStore.userInfo.name,
name: userStore.userInfo.username,
schoolFacultyId: "平台级账号",
schoolMajorId: "平台级账号",
schoolClassId: "无",
password: "******",
passwords: "",
};
return;
}
indexApi
.getInfo({ userId: userStore.userInfo.userId })
.then((res) => {
form.value = res.data;
})
@ -262,18 +297,33 @@ onMounted(() => {
font-size: 20px;
color: #fff;
font-weight: bold;
display: flex;
gap: 30px;
.el-ment-item {
cursor: pointer;
&:hover {
color: #409eff;
}
}
.el-ment-item-active {
color: #409eff;
}
}
display: flex;
gap: 18px;
align-items: center;
white-space: nowrap;
.el-ment-item {
padding: 6px 16px;
border: 1px solid transparent;
border-radius: 999px;
cursor: pointer;
line-height: 1;
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
color: #9ee7ff;
border-color: rgba(0, 170, 255, 0.42);
background: rgba(0, 124, 204, 0.18);
}
}
.el-ment-item-active {
color: #9ee7ff;
border-color: rgba(0, 170, 255, 0.68);
background: linear-gradient(90deg, rgba(0, 143, 254, 0.32), rgba(10, 92, 134, 0.2));
box-shadow: inset 0 0 12px rgba(0, 170, 255, 0.12);
}
&.platform-nav {
gap: 14px;
}
}
.right-menu {
float: right;
height: 100%;

@ -24,7 +24,7 @@ import useAppStore from "@/store/modules/app";
import * as indexApi from "@/api/teacher";
import useSettingsStore from "@/store/modules/settings";
import usePermissionStore from "@/store/modules/permission";
import { constantRoutes, dynamicRoutes, teacherRoutes } from "@/router/index.js";
import { constantRoutes, dynamicRoutes, teacherRoutes, platformAdminRoutes, schoolAdminRoutes } from "@/router/index.js";
import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@ -319,10 +319,14 @@ function excludeRoutesByMetaTitle(routes, excludeTitles) {
return result;
}, []);
}
const getTaskList = () => {
if (userStore.userInfo.roleId != 3) {
indexApi.getTaskList({ classId: userStore.userInfo.classId }).then((res) => {
const disabledTitles = processModuleStatus(groupAndSortModules(res.data)).disabledParents;
const getTaskList = () => {
if (userStore.userInfo.roleId == 1) {
sidebarRouters.value = [...constantRoutes, ...platformAdminRoutes];
} else if (userStore.userInfo.roleId == 2) {
sidebarRouters.value = [...constantRoutes, ...schoolAdminRoutes];
} else if (userStore.userInfo.roleId != 3) {
indexApi.getTaskList({ classId: userStore.userInfo.classId }).then((res) => {
const disabledTitles = processModuleStatus(groupAndSortModules(res.data)).disabledParents;
console.log(processModuleStatus(groupAndSortModules(res.data)));
proxy.$cache.session.setJSON("disabledChildren", processModuleStatus(groupAndSortModules(res.data)).disabledChildren);
proxy.$cache.session.setJSON("disabledTitles", disabledTitles);

@ -4,8 +4,8 @@
<Navbar />
</div>
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar v-if="!sidebar.hide" class="sidebar-container" :class="[role === 3 ? 'newImg' : '']" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<sidebar v-if="!sidebar.hide && !useTopNavbarOnly" class="sidebar-container" :class="[role === 3 ? 'newImg' : '']" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide || useTopNavbarOnly }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }"></div>
<app-main />
<settings ref="settingRef" />
@ -22,14 +22,15 @@ import { getUserInfo } from "@/utils/auth";
import useAppStore from "@/store/modules/app";
import useSettingsStore from "@/store/modules/settings";
const role = ref(JSON.parse(getUserInfo()).roleId);
const role = ref(Number(JSON.parse(getUserInfo()).roleId));
const settingsStore = useSettingsStore();
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const useTopNavbarOnly = computed(() => role.value === 1);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,

@ -23,11 +23,17 @@ const whiteList = ["/login", "/register"];
router.beforeEach((to, from, next) => {
NProgress.start();
if (getToken()) {
if (to.path === "/login") {
if (JSON.parse(getUserInfo()).roleId == 3) {
next({ path: "/index" });
NProgress.done();
} else if (JSON.parse(getUserInfo()).roleId == 4) {
if (to.path === "/login") {
if (JSON.parse(getUserInfo()).roleId == 1) {
next({ path: "/index" });
NProgress.done();
} else if (JSON.parse(getUserInfo()).roleId == 2) {
next({ path: "/index" });
NProgress.done();
} else if (JSON.parse(getUserInfo()).roleId == 3) {
next({ path: "/index" });
NProgress.done();
} else if (JSON.parse(getUserInfo()).roleId == 4) {
next({ path: "/index" });
NProgress.done();
}

@ -12,10 +12,17 @@ function authPermission(permission) {
}
}
function authRole(role) {
const super_admin = "admin";
const roles = [parseFloat(JSON.parse(getUserInfo()).roleId) ==3 ? 'teacher':'student']
if (role && role.length > 0) {
function authRole(role) {
const super_admin = "admin";
const roleMap = {
1: "platformAdmin",
2: "schoolAdmin",
3: "teacher",
4: "student",
};
const roleId = Number(JSON.parse(getUserInfo()).roleId);
const roles = [roleMap[roleId]].filter(Boolean);
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})

@ -388,7 +388,7 @@ export const dynamicRoutes = [
},
];
// 教师端路由
export const teacherRoutes = [
export const teacherRoutes = [
{
path: "/school",
component: Layout,
@ -507,10 +507,134 @@ export const teacherRoutes = [
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes: [...constantRoutes, ...dynamicRoutes, ...teacherRoutes],
];
export const platformAdminRoutes = [
{
path: "/platform-schools",
component: Layout,
redirect: "/platform-schools/index",
roles: ["platformAdmin"],
meta: { title: "学校管理", icon: "学校", affix: true },
children: [
{
path: "index",
component: () => import("@/views/platformAdmin/school/index.vue"),
name: "platformSchools",
meta: { title: "学校管理", icon: "学校", affix: true },
},
],
},
{
path: "/platform-school-admins",
component: Layout,
redirect: "/platform-school-admins/index",
roles: ["platformAdmin"],
meta: { title: "学校管理员", icon: "用户", affix: true },
children: [
{
path: "index",
component: () => import("@/views/platformAdmin/schoolAdmin/index.vue"),
name: "platformSchoolAdmins",
meta: { title: "学校管理员", icon: "用户", affix: true },
},
],
},
];
export const schoolAdminRoutes = [
{
path: "/admin-school",
component: Layout,
redirect: "/admin-school/index",
roles: ["schoolAdmin"],
meta: { title: "学校管理", icon: "学校", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/school/index.vue"),
name: "adminSchool",
meta: { title: "学校管理", icon: "学校", affix: true },
},
],
},
{
path: "/admin-faculty",
component: Layout,
redirect: "/admin-faculty/index",
roles: ["schoolAdmin"],
meta: { title: "院系管理", icon: "院系管理", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/faculty/index.vue"),
name: "adminFaculty",
meta: { title: "院系管理", icon: "院系管理", affix: true },
},
],
},
{
path: "/admin-major",
component: Layout,
redirect: "/admin-major/index",
roles: ["schoolAdmin"],
meta: { title: "专业管理", icon: "专业管理", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/major/index.vue"),
name: "adminMajor",
meta: { title: "专业管理", icon: "专业管理", affix: true },
},
],
},
{
path: "/admin-class",
component: Layout,
redirect: "/admin-class/index",
roles: ["schoolAdmin"],
meta: { title: "班级管理", icon: "班级管理", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/class/index.vue"),
name: "adminClass",
meta: { title: "班级管理", icon: "班级管理", affix: true },
},
],
},
{
path: "/admin-teacher",
component: Layout,
redirect: "/admin-teacher/index",
roles: ["schoolAdmin"],
meta: { title: "教师管理", icon: "教师管理", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/teacher/index.vue"),
name: "adminTeacher",
meta: { title: "教师管理", icon: "教师管理", affix: true },
},
],
},
{
path: "/admin-student",
component: Layout,
redirect: "/admin-student/index",
roles: ["schoolAdmin"],
meta: { title: "学生管理", icon: "学生管理", affix: true },
children: [
{
path: "index",
component: () => import("@/views/schoolAdmin/student/index.vue"),
name: "adminStudent",
meta: { title: "学生管理", icon: "学生管理", affix: true },
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes: [...constantRoutes, ...dynamicRoutes, ...teacherRoutes, ...platformAdminRoutes, ...schoolAdminRoutes],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;

@ -0,0 +1,117 @@
export const chinaRegions = [
{ province: "北京市", cities: ["北京市"] },
{ province: "天津市", cities: ["天津市"] },
{
province: "河北省",
cities: ["石家庄市", "唐山市", "秦皇岛市", "邯郸市", "邢台市", "保定市", "张家口市", "承德市", "沧州市", "廊坊市", "衡水市"],
},
{
province: "山西省",
cities: ["太原市", "大同市", "阳泉市", "长治市", "晋城市", "朔州市", "晋中市", "运城市", "忻州市", "临汾市", "吕梁市"],
},
{
province: "内蒙古自治区",
cities: ["呼和浩特市", "包头市", "乌海市", "赤峰市", "通辽市", "鄂尔多斯市", "呼伦贝尔市", "巴彦淖尔市", "乌兰察布市", "兴安盟", "锡林郭勒盟", "阿拉善盟"],
},
{
province: "辽宁省",
cities: ["沈阳市", "大连市", "鞍山市", "抚顺市", "本溪市", "丹东市", "锦州市", "营口市", "阜新市", "辽阳市", "盘锦市", "铁岭市", "朝阳市", "葫芦岛市"],
},
{
province: "吉林省",
cities: ["长春市", "吉林市", "四平市", "辽源市", "通化市", "白山市", "松原市", "白城市", "延边朝鲜族自治州"],
},
{
province: "黑龙江省",
cities: ["哈尔滨市", "齐齐哈尔市", "鸡西市", "鹤岗市", "双鸭山市", "大庆市", "伊春市", "佳木斯市", "七台河市", "牡丹江市", "黑河市", "绥化市", "大兴安岭地区"],
},
{ province: "上海市", cities: ["上海市"] },
{
province: "江苏省",
cities: ["南京市", "无锡市", "徐州市", "常州市", "苏州市", "南通市", "连云港市", "淮安市", "盐城市", "扬州市", "镇江市", "泰州市", "宿迁市"],
},
{
province: "浙江省",
cities: ["杭州市", "宁波市", "温州市", "嘉兴市", "湖州市", "绍兴市", "金华市", "衢州市", "舟山市", "台州市", "丽水市"],
},
{
province: "安徽省",
cities: ["合肥市", "芜湖市", "蚌埠市", "淮南市", "马鞍山市", "淮北市", "铜陵市", "安庆市", "黄山市", "滁州市", "阜阳市", "宿州市", "六安市", "亳州市", "池州市", "宣城市"],
},
{
province: "福建省",
cities: ["福州市", "厦门市", "莆田市", "三明市", "泉州市", "漳州市", "南平市", "龙岩市", "宁德市"],
},
{
province: "江西省",
cities: ["南昌市", "景德镇市", "萍乡市", "九江市", "新余市", "鹰潭市", "赣州市", "吉安市", "宜春市", "抚州市", "上饶市"],
},
{
province: "山东省",
cities: ["济南市", "青岛市", "淄博市", "枣庄市", "东营市", "烟台市", "潍坊市", "济宁市", "泰安市", "威海市", "日照市", "临沂市", "德州市", "聊城市", "滨州市", "菏泽市"],
},
{
province: "河南省",
cities: ["郑州市", "开封市", "洛阳市", "平顶山市", "安阳市", "鹤壁市", "新乡市", "焦作市", "濮阳市", "许昌市", "漯河市", "三门峡市", "南阳市", "商丘市", "信阳市", "周口市", "驻马店市", "济源市"],
},
{
province: "湖北省",
cities: ["武汉市", "黄石市", "十堰市", "宜昌市", "襄阳市", "鄂州市", "荆门市", "孝感市", "荆州市", "黄冈市", "咸宁市", "随州市", "恩施土家族苗族自治州", "仙桃市", "潜江市", "天门市", "神农架林区"],
},
{
province: "湖南省",
cities: ["长沙市", "株洲市", "湘潭市", "衡阳市", "邵阳市", "岳阳市", "常德市", "张家界市", "益阳市", "郴州市", "永州市", "怀化市", "娄底市", "湘西土家族苗族自治州"],
},
{
province: "广东省",
cities: ["广州市", "韶关市", "深圳市", "珠海市", "汕头市", "佛山市", "江门市", "湛江市", "茂名市", "肇庆市", "惠州市", "梅州市", "汕尾市", "河源市", "阳江市", "清远市", "东莞市", "中山市", "潮州市", "揭阳市", "云浮市"],
},
{
province: "广西壮族自治区",
cities: ["南宁市", "柳州市", "桂林市", "梧州市", "北海市", "防城港市", "钦州市", "贵港市", "玉林市", "百色市", "贺州市", "河池市", "来宾市", "崇左市"],
},
{
province: "海南省",
cities: ["海口市", "三亚市", "三沙市", "儋州市", "五指山市", "琼海市", "文昌市", "万宁市", "东方市", "定安县", "屯昌县", "澄迈县", "临高县", "白沙黎族自治县", "昌江黎族自治县", "乐东黎族自治县", "陵水黎族自治县", "保亭黎族苗族自治县", "琼中黎族苗族自治县"],
},
{ province: "重庆市", cities: ["重庆市"] },
{
province: "四川省",
cities: ["成都市", "自贡市", "攀枝花市", "泸州市", "德阳市", "绵阳市", "广元市", "遂宁市", "内江市", "乐山市", "南充市", "眉山市", "宜宾市", "广安市", "达州市", "雅安市", "巴中市", "资阳市", "阿坝藏族羌族自治州", "甘孜藏族自治州", "凉山彝族自治州"],
},
{
province: "贵州省",
cities: ["贵阳市", "六盘水市", "遵义市", "安顺市", "毕节市", "铜仁市", "黔西南布依族苗族自治州", "黔东南苗族侗族自治州", "黔南布依族苗族自治州"],
},
{
province: "云南省",
cities: ["昆明市", "曲靖市", "玉溪市", "保山市", "昭通市", "丽江市", "普洱市", "临沧市", "楚雄彝族自治州", "红河哈尼族彝族自治州", "文山壮族苗族自治州", "西双版纳傣族自治州", "大理白族自治州", "德宏傣族景颇族自治州", "怒江傈僳族自治州", "迪庆藏族自治州"],
},
{
province: "西藏自治区",
cities: ["拉萨市", "日喀则市", "昌都市", "林芝市", "山南市", "那曲市", "阿里地区"],
},
{
province: "陕西省",
cities: ["西安市", "铜川市", "宝鸡市", "咸阳市", "渭南市", "延安市", "汉中市", "榆林市", "安康市", "商洛市"],
},
{
province: "甘肃省",
cities: ["兰州市", "嘉峪关市", "金昌市", "白银市", "天水市", "武威市", "张掖市", "平凉市", "酒泉市", "庆阳市", "定西市", "陇南市", "临夏回族自治州", "甘南藏族自治州"],
},
{
province: "青海省",
cities: ["西宁市", "海东市", "海北藏族自治州", "黄南藏族自治州", "海南藏族自治州", "果洛藏族自治州", "玉树藏族自治州", "海西蒙古族藏族自治州"],
},
{ province: "宁夏回族自治区", cities: ["银川市", "石嘴山市", "吴忠市", "固原市", "中卫市"] },
{
province: "新疆维吾尔自治区",
cities: ["乌鲁木齐市", "克拉玛依市", "吐鲁番市", "哈密市", "昌吉回族自治州", "博尔塔拉蒙古自治州", "巴音郭楞蒙古自治州", "阿克苏地区", "克孜勒苏柯尔克孜自治州", "喀什地区", "和田地区", "伊犁哈萨克自治州", "塔城地区", "阿勒泰地区", "石河子市", "阿拉尔市", "图木舒克市", "五家渠市", "北屯市", "铁门关市", "双河市", "可克达拉市", "昆玉市", "胡杨河市", "新星市", "白杨市"],
},
{
province: "台湾省",
cities: ["台北市", "新北市", "桃园市", "台中市", "台南市", "高雄市", "基隆市", "新竹市", "嘉义市", "新竹县", "苗栗县", "彰化县", "南投县", "云林县", "嘉义县", "屏东县", "宜兰县", "花莲县", "台东县", "澎湖县", "金门县", "连江县"],
},
{ province: "香港特别行政区", cities: ["香港岛", "九龙", "新界"] },
{ province: "澳门特别行政区", cities: ["澳门半岛", "氹仔", "路环"] },
];

@ -1,19 +1,113 @@
<template>
<div class="index">
<student v-if="role == '4'" />
<teacher v-else />
</div>
</template>
<script setup>
import student from "./student/index.vue";
import teacher from "./teacherEnd/index.vue";
import useUserStore from "@/store/modules/user";
import { computed } from "vue";
const userStore = useUserStore();
const role = computed(() => userStore.userInfo.roleId);
</script>
<style lang='scss' scoped>
.index {
margin: 10px;
}
</style>
<template>
<div class="index">
<div v-if="role == '1'" class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">Platform Admin / Console</div>
<h1 class="admin-title">平台超管工作台</h1>
<p class="admin-subtitle">统一维护学校档案和学校管理员账号,学校内部数据交给学校管理员处理。</p>
</section>
<section class="admin-panel">
<div class="quick-grid">
<div class="quick-card" @click="go('/platform-schools/index')">
<div class="quick-label">Schools</div>
<h2>学校管理</h2>
<p>新增、编辑、删除学校基础信息。</p>
<el-button type="primary">进入管理</el-button>
</div>
<div class="quick-card" @click="go('/platform-school-admins/index')">
<div class="quick-label">Admins</div>
<h2>学校管理员</h2>
<p>为每所学校维护 role=2 管理员账号。</p>
<el-button type="primary">进入管理</el-button>
</div>
</div>
</section>
</div>
<div v-else-if="role == '2'" class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin / Console</div>
<h1 class="admin-title">学校管理员工作台</h1>
<p class="admin-subtitle">维护本校组织架构、教师和学生账号,所有数据限定在当前学校。</p>
</section>
<section class="admin-panel">
<div class="quick-grid">
<div v-for="item in schoolAdminLinks" :key="item.path" class="quick-card" @click="go(item.path)">
<div class="quick-label">{{ item.label }}</div>
<h2>{{ item.title }}</h2>
<p>{{ item.desc }}</p>
<el-button type="primary">进入管理</el-button>
</div>
</div>
</section>
</div>
<teacher v-else-if="role == '3'" />
<student v-else />
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import student from "./student/index.vue";
import teacher from "./teacherEnd/index.vue";
import useUserStore from "@/store/modules/user";
import { computed } from "vue";
const userStore = useUserStore();
const router = useRouter();
const role = computed(() => userStore.userInfo.roleId);
const go = (path) => {
router.push(path);
};
const schoolAdminLinks = [
{ label: "School", title: "学校管理", desc: "查看并编辑当前学校基础信息。", path: "/admin-school/index" },
{ label: "Faculty", title: "院系管理", desc: "维护本校院系数据。", path: "/admin-faculty/index" },
{ label: "Major", title: "专业管理", desc: "维护本校专业数据。", path: "/admin-major/index" },
{ label: "Class", title: "班级管理", desc: "维护本校班级数据。", path: "/admin-class/index" },
{ label: "Teacher", title: "教师管理", desc: "维护本校教师账号。", path: "/admin-teacher/index" },
{ label: "Student", title: "学生管理", desc: "维护本校学生账号。", path: "/admin-student/index" },
];
</script>
<style lang='scss' scoped>
.index {
margin: 10px;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 18px;
}
.quick-card {
min-height: 178px;
padding: 22px;
border: 1px solid rgba(80, 172, 255, 0.28);
border-radius: 8px;
background: linear-gradient(145deg, rgba(10, 25, 54, 0.92), rgba(7, 18, 42, 0.78));
box-shadow: inset 0 0 24px rgba(35, 122, 255, 0.12), 0 16px 34px rgba(0, 0, 0, 0.24);
cursor: pointer;
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.quick-card:hover {
transform: translateY(-3px);
border-color: rgba(83, 199, 255, 0.72);
box-shadow: inset 0 0 30px rgba(35, 122, 255, 0.18), 0 22px 44px rgba(0, 0, 0, 0.32);
}
.quick-card h2 {
margin: 10px 0 8px;
color: #f7fbff;
font-size: 22px;
font-weight: 700;
}
.quick-card p {
min-height: 44px;
margin: 0 0 18px;
color: rgba(221, 236, 255, 0.72);
line-height: 1.6;
}
.quick-label {
display: inline-flex;
padding: 4px 10px;
border-radius: 999px;
color: #76dcff;
font-size: 12px;
background: rgba(73, 178, 255, 0.12);
border: 1px solid rgba(73, 178, 255, 0.26);
}
</style>

@ -0,0 +1,208 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">Platform Admin / Schools</div>
<h1 class="admin-title">学校管理</h1>
<p class="admin-subtitle">平台超管维护学校基础档案,学校管理员账号和本校业务数据会绑定到这里。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.schoolName" clearable placeholder="学校名称" class="filter-input" @keyup.enter="load" />
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="学校名称" prop="schoolName" min-width="180" />
<el-table-column label="学校层次" prop="schoolGrade" min-width="120" />
<el-table-column label="省份" prop="schoolProvince" width="110" />
<el-table-column label="城市" prop="schoolCity" width="110" />
<el-table-column label="联系人" prop="schoolContacts" min-width="120" />
<el-table-column label="联系电话" prop="schoolContactsMobile" min-width="140" />
<el-table-column label="联系邮箱" prop="schoolContactsEmail" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pager"
layout="total, prev, pager, next, sizes"
:total="total"
v-model:current-page="query.index"
v-model:page-size="query.size"
@current-change="load"
@size-change="handleSizeChange"
/>
<el-dialog
v-model="dialogVisible"
:title="editingId ? '编辑学校' : '新增学校'"
width="680px"
align-center
append-to-body
custom-class="platform-admin-dialog platform-school-dialog"
>
<el-form :model="form" label-width="98px">
<el-form-item label="学校名称">
<el-input v-model="form.schoolName" />
</el-form-item>
<el-form-item label="学校层次">
<el-input v-model="form.schoolGrade" placeholder="如:本科、专科、双一流" />
</el-form-item>
<el-form-item label="省份/城市">
<div class="form-inline">
<el-select v-model="form.schoolProvince" filterable placeholder="请选择省份" @change="handleProvinceChange">
<el-option v-for="item in chinaRegions" :key="item.province" :label="item.province" :value="item.province" />
</el-select>
<el-select v-model="form.schoolCity" filterable placeholder="请选择城市" :disabled="!form.schoolProvince">
<el-option v-for="city in cityOptions" :key="city" :label="city" :value="city" />
</el-select>
</div>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="form.schoolContacts" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.schoolContactsMobile" />
</el-form-item>
<el-form-item label="联系邮箱">
<el-input v-model="form.schoolContactsEmail" />
</el-form-item>
<el-form-item label="学校Logo">
<ImageUpload v-model="form.schoolLogo" class="school-logo-upload" :limit="1" :file-size="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addPlatformSchool, deletePlatformSchool, getPlatformSchools, updatePlatformSchool } from "@/api/platformAdmin";
import { chinaRegions } from "@/utils/chinaRegions";
const query = reactive({ index: 1, size: 10, schoolName: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const editingId = ref("");
const form = ref({});
const cityOptions = computed(() => chinaRegions.find((item) => item.province === form.value.schoolProvince)?.cities || []);
const load = async () => {
loading.value = true;
try {
const res = await getPlatformSchools(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.schoolName = "";
load();
};
const handleSizeChange = () => {
query.index = 1;
load();
};
const openCreate = () => {
editingId.value = "";
form.value = {
schoolName: "",
schoolGrade: "",
schoolProvince: "",
schoolCity: "",
schoolContacts: "",
schoolContactsMobile: "",
schoolContactsEmail: "",
schoolLogo: "",
};
dialogVisible.value = true;
};
const openEdit = (row) => {
editingId.value = row.schoolId;
form.value = { ...row };
dialogVisible.value = true;
};
const handleProvinceChange = () => {
form.value.schoolCity = "";
};
const buildSchoolPayload = () => ({
schoolName: form.value.schoolName,
schoolGrade: form.value.schoolGrade,
schoolProvince: form.value.schoolProvince,
schoolCity: form.value.schoolCity,
schoolContacts: form.value.schoolContacts,
schoolContactsMobile: form.value.schoolContactsMobile,
schoolContactsEmail: form.value.schoolContactsEmail,
schoolLogo: form.value.schoolLogo,
});
const submit = async () => {
const payload = buildSchoolPayload();
if (editingId.value) {
await updatePlatformSchool(editingId.value, payload);
} else {
await addPlatformSchool(payload);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该学校?学校下存在业务数据时后端会拒绝删除。", "提示", { type: "warning" });
await deletePlatformSchool(row.schoolId);
ElMessage.success("删除成功");
load();
};
onMounted(load);
</script>
<style scoped lang="scss">
.form-inline {
display: grid;
width: 100%;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.school-logo-upload {
width: 100%;
}
.school-logo-upload :deep(.el-upload-list--picture-card .el-upload-list__item),
.school-logo-upload :deep(.el-upload--picture-card) {
width: 96px;
height: 96px;
}
@media (max-width: 640px) {
.form-inline {
grid-template-columns: 1fr;
}
}
</style>

@ -0,0 +1,176 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">Platform Admin / School Admins</div>
<h1 class="admin-title">学校管理员</h1>
<p class="admin-subtitle">为学校分配管理员账号,账号只能绑定到指定学校,后端会固定保存为学校管理员角色。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-select v-model="query.schoolId" clearable filterable placeholder="所属学校" class="filter-input">
<el-option v-for="school in schools" :key="school.schoolId" :label="school.schoolName" :value="school.schoolId" />
</el-select>
<el-input v-model="query.name" clearable placeholder="姓名" class="filter-input" @keyup.enter="load" />
<el-input v-model="query.username" clearable placeholder="账号" class="filter-input" @keyup.enter="load" />
<el-input v-model="query.phone" clearable placeholder="手机号" class="filter-input" @keyup.enter="load" />
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="姓名" prop="name" min-width="120" />
<el-table-column label="账号" prop="username" min-width="140" />
<el-table-column label="所属学校" min-width="180">
<template #default="{ row }">{{ schoolName(row.schoolId) }}</template>
</el-table-column>
<el-table-column label="性别" prop="sex" width="90" />
<el-table-column label="手机号" prop="phone" min-width="140" />
<el-table-column label="邮箱" prop="email" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pager"
layout="total, prev, pager, next, sizes"
:total="total"
v-model:current-page="query.index"
v-model:page-size="query.size"
@current-change="load"
@size-change="handleSizeChange"
/>
<el-dialog
v-model="dialogVisible"
:title="form.userId ? '编辑学校管理员' : '新增学校管理员'"
width="560px"
align-center
append-to-body
custom-class="platform-admin-dialog"
>
<el-form :model="form" label-width="92px">
<el-form-item label="所属学校">
<el-select v-model="form.schoolId" filterable placeholder="请选择学校" style="width: 100%">
<el-option v-for="school in schools" :key="school.schoolId" :label="school.schoolName" :value="school.schoolId" />
</el-select>
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="账号">
<el-input v-model="form.username" :disabled="Boolean(form.userId)" />
</el-form-item>
<el-form-item label="角色">
<el-input model-value="学校管理员" disabled />
</el-form-item>
<el-form-item label="性别">
<el-input v-model="form.sex" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="form.email" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import {
addPlatformSchoolAdmin,
deletePlatformSchoolAdmin,
getPlatformSchoolAdmins,
getPlatformSchools,
updatePlatformSchoolAdmin,
} from "@/api/platformAdmin";
const query = reactive({ index: 1, size: 10, schoolId: "", name: "", username: "", phone: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const schools = ref([]);
const schoolName = (schoolId) => {
return schools.value.find((school) => school.schoolId === schoolId)?.schoolName || schoolId || "-";
};
const loadSchools = async () => {
const res = await getPlatformSchools({ index: 1, size: 1000 });
schools.value = res.data?.list || [];
};
const load = async () => {
loading.value = true;
try {
const res = await getPlatformSchoolAdmins(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.schoolId = "";
query.name = "";
query.username = "";
query.phone = "";
load();
};
const handleSizeChange = () => {
query.index = 1;
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...row };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.userId) {
await updatePlatformSchoolAdmin(form.value.userId, form.value);
} else {
await addPlatformSchoolAdmin(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该学校管理员?", "提示", { type: "warning" });
await deletePlatformSchoolAdmin(row.userId);
ElMessage.success("删除成功");
load();
};
onMounted(async () => {
await loadSchools();
await load();
});
</script>

@ -0,0 +1,212 @@
.school-admin-workbench {
min-height: calc(100vh - 84px);
padding: 24px;
color: #eef5ff;
background:
radial-gradient(circle at 12% 16%, rgba(0, 170, 255, 0.24), transparent 28%),
radial-gradient(circle at 85% 4%, rgba(43, 110, 255, 0.16), transparent 28%),
linear-gradient(135deg, #07101d 0%, #03060c 100%);
}
.admin-hero {
position: relative;
overflow: hidden;
margin-bottom: 18px;
padding: 22px 24px;
border: 1px solid rgba(0, 180, 255, 0.32);
border-radius: 24px;
background: rgba(8, 18, 28, 0.72);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(14px);
}
.admin-hero::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
linear-gradient(90deg, rgba(0, 170, 255, 0.16), transparent 38%),
repeating-linear-gradient(90deg, rgba(125, 211, 252, 0.08) 0 1px, transparent 1px 64px);
opacity: 0.42;
}
.admin-badge {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 5px 14px;
border: 1px solid rgba(0, 170, 255, 0.55);
border-radius: 999px;
color: #b9e2ff;
background: rgba(0, 170, 255, 0.15);
font-size: 12px;
font-weight: 600;
}
.admin-title {
position: relative;
z-index: 1;
margin: 0;
font-size: 28px;
line-height: 1.2;
font-weight: 800;
letter-spacing: 0;
color: transparent;
background: linear-gradient(135deg, #ffffff, #8bcbff);
-webkit-background-clip: text;
background-clip: text;
}
.admin-subtitle {
position: relative;
z-index: 1;
margin-top: 8px;
color: #9ab3d0;
font-size: 14px;
}
.admin-panel {
padding: 18px;
border: 1px solid rgba(0, 180, 255, 0.26);
border-radius: 24px;
background: rgba(8, 18, 28, 0.7);
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.24);
backdrop-filter: blur(12px);
}
.school-admin-workbench .toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 14px;
padding: 12px;
border: 1px solid rgba(45, 143, 207, 0.36);
border-radius: 18px;
background: rgba(0, 35, 55, 0.44);
}
.school-admin-workbench .filter-input {
width: 220px;
}
.school-admin-workbench .el-input__wrapper,
.school-admin-workbench .el-select .el-input__wrapper {
background: rgba(3, 24, 36, 0.9);
border: 1px solid rgba(45, 95, 126, 0.9);
box-shadow: none;
}
.school-admin-workbench .el-input__inner {
color: #eef5ff;
}
.school-admin-workbench .el-input__inner::placeholder {
color: #5f7d9c;
}
.school-admin-workbench .el-button {
border-radius: 14px;
font-weight: 600;
}
.school-admin-workbench .el-button--primary,
.school-admin-workbench .el-button--success {
border-color: rgba(0, 170, 255, 0.55);
background: linear-gradient(95deg, #1175a8, #0a5c86);
box-shadow: 0 0 14px rgba(0, 170, 255, 0.16);
}
.school-admin-workbench .el-table {
overflow: hidden;
border-radius: 18px;
border-color: rgba(45, 143, 207, 0.32);
background: rgba(3, 24, 36, 0.72);
color: #dceeff;
}
.school-admin-workbench .el-table th.el-table__cell {
background: rgba(0, 42, 68, 0.92);
color: #b9e2ff;
font-weight: 700;
}
.school-admin-workbench .el-table tr,
.school-admin-workbench .el-table td.el-table__cell {
background: rgba(3, 24, 36, 0.58);
border-color: rgba(45, 143, 207, 0.18);
}
.school-admin-workbench .el-table__body tr:hover > td.el-table__cell {
background: rgba(0, 160, 255, 0.14);
}
.school-admin-workbench .pager {
margin-top: 14px;
justify-content: flex-end;
}
.school-admin-workbench .el-pagination {
--el-pagination-bg-color: transparent;
--el-pagination-button-bg-color: rgba(3, 24, 36, 0.85);
--el-pagination-hover-color: #00aaff;
color: #cde4ff;
}
.school-admin-workbench .admin-form {
max-width: none;
padding: 18px;
border: 1px solid rgba(45, 143, 207, 0.32);
border-radius: 22px;
background: rgba(0, 20, 35, 0.62);
}
.school-admin-workbench .el-form-item__label {
color: #b9e2ff;
font-weight: 600;
}
.el-dialog {
border-radius: 22px;
background: #07101d;
border: 1px solid rgba(0, 180, 255, 0.28);
}
.el-dialog__title,
.el-dialog .el-form-item__label {
color: #eef5ff;
}
.el-dialog .el-dialog__body,
.el-dialog .el-dialog__footer {
background: transparent;
}
.platform-admin-dialog {
display: flex;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 48px);
flex-direction: column;
}
.platform-admin-dialog .el-dialog__body {
overflow-y: auto;
}
@media (max-width: 900px) {
.school-admin-workbench {
padding: 14px;
}
.admin-title {
font-size: 23px;
}
.school-admin-workbench .filter-input {
width: 100%;
}
}

@ -0,0 +1,122 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Class</div>
<h1 class="admin-title">班级管理</h1>
<p class="admin-subtitle">维护本校班级与专业关系,班级删除会受学生和任务数据保护。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.className" clearable placeholder="班级名称" class="filter-input" @keyup.enter="load" />
<el-select v-model="query.schoolMajorId" clearable placeholder="所属专业" class="filter-input">
<el-option v-for="major in majors" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="班级名称" min-width="180">
<template #default="{ row }">{{ item(row).className }}</template>
</el-table-column>
<el-table-column label="班级编号" min-width="140">
<template #default="{ row }">{{ item(row).classSn }}</template>
</el-table-column>
<el-table-column label="所属专业" min-width="180">
<template #default="{ row }">{{ majorName(item(row).schoolMajorId) }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pager" layout="total, prev, pager, next, sizes" :total="total" v-model:current-page="query.index" v-model:page-size="query.size" @current-change="load" @size-change="load" />
<el-dialog v-model="dialogVisible" :title="form.schoolClassId ? '编辑班级' : '新增班级'" width="520px">
<el-form :model="form" label-width="96px">
<el-form-item label="所属专业">
<el-select v-model="form.schoolMajorId" placeholder="请选择专业" style="width: 100%">
<el-option v-for="major in majors" :key="major.schoolMajorId" :label="major.schoolMajorName" :value="major.schoolMajorId" />
</el-select>
</el-form-item>
<el-form-item label="班级名称"><el-input v-model="form.className" /></el-form-item>
<el-form-item label="班级编号"><el-input v-model="form.classSn" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addAdminClass, deleteAdminClass, getAdminClasses, getAdminMajors, updateAdminClass } from "@/api/schoolAdmin";
const query = reactive({ index: 1, size: 10, className: "", schoolMajorId: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const majors = ref([]);
const item = (row) => row.schoolClass || row;
const majorName = (id) => majors.value.find((major) => major.schoolMajorId === id)?.schoolMajorName || "";
const loadMajors = async () => {
const res = await getAdminMajors({ index: 1, size: 1000 });
majors.value = (res.data?.list || []).map((row) => ({ ...(row.schoolMajor || row), schoolMajorId: row.schoolMajorId || row.schoolMajor?.schoolMajorId }));
};
const load = async () => {
loading.value = true;
try {
const res = await getAdminClasses(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.className = "";
query.schoolMajorId = "";
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...item(row) };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.schoolClassId) {
await updateAdminClass(form.value.schoolClassId, form.value);
} else {
await addAdminClass(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该班级?", "提示", { type: "warning" });
await deleteAdminClass(item(row).schoolClassId);
ElMessage.success("删除成功");
load();
};
onMounted(() => {
loadMajors();
load();
});
</script>

@ -0,0 +1,109 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Faculty</div>
<h1 class="admin-title">院系管理</h1>
<p class="admin-subtitle">按本校组织结构维护院系信息,支撑专业、教师和学生归属。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.schoolFacultyName" clearable placeholder="院系名称" class="filter-input" @keyup.enter="load" />
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="院系名称" min-width="180">
<template #default="{ row }">{{ item(row).schoolFacultyName }}</template>
</el-table-column>
<el-table-column label="联系人" min-width="120">
<template #default="{ row }">{{ item(row).schoolFacultyContact }}</template>
</el-table-column>
<el-table-column label="联系电话" min-width="140">
<template #default="{ row }">{{ item(row).schoolFacultyContactTel }}</template>
</el-table-column>
<el-table-column label="联系地址" min-width="180">
<template #default="{ row }">{{ item(row).schoolFacultyContactAddress }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pager" layout="total, prev, pager, next, sizes" :total="total" v-model:current-page="query.index" v-model:page-size="query.size" @current-change="load" @size-change="load" />
<el-dialog v-model="dialogVisible" :title="form.schoolFacultyId ? '编辑院系' : '新增院系'" width="520px">
<el-form :model="form" label-width="96px">
<el-form-item label="院系名称"><el-input v-model="form.schoolFacultyName" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="form.schoolFacultyContact" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.schoolFacultyContactTel" /></el-form-item>
<el-form-item label="联系地址"><el-input v-model="form.schoolFacultyContactAddress" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addAdminFaculty, deleteAdminFaculty, getAdminFaculties, updateAdminFaculty } from "@/api/schoolAdmin";
const query = reactive({ index: 1, size: 10, schoolFacultyName: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const item = (row) => row.schoolFaculty || row;
const load = async () => {
loading.value = true;
try {
const res = await getAdminFaculties(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.schoolFacultyName = "";
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...item(row) };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.schoolFacultyId) {
await updateAdminFaculty(form.value.schoolFacultyId, form.value);
} else {
await addAdminFaculty(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该院系?", "提示", { type: "warning" });
await deleteAdminFaculty(item(row).schoolFacultyId);
ElMessage.success("删除成功");
load();
};
onMounted(load);
</script>

@ -0,0 +1,123 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Major</div>
<h1 class="admin-title">专业管理</h1>
<p class="admin-subtitle">维护本校院系下的专业档案,专业数据会被班级和学生管理联动使用。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.schoolMajorName" clearable placeholder="专业名称" class="filter-input" @keyup.enter="load" />
<el-select v-model="query.schoolFacultyId" clearable placeholder="所属院系" class="filter-input">
<el-option v-for="faculty in faculties" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="专业名称" min-width="180">
<template #default="{ row }">{{ item(row).schoolMajorName }}</template>
</el-table-column>
<el-table-column label="所属院系" prop="schoolFacultyName" min-width="160" />
<el-table-column label="联系人" min-width="120">
<template #default="{ row }">{{ item(row).schoolMajorContact }}</template>
</el-table-column>
<el-table-column label="联系电话" min-width="140">
<template #default="{ row }">{{ item(row).schoolMajorContactTel }}</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pager" layout="total, prev, pager, next, sizes" :total="total" v-model:current-page="query.index" v-model:page-size="query.size" @current-change="load" @size-change="load" />
<el-dialog v-model="dialogVisible" :title="form.schoolMajorId ? '编辑专业' : '新增专业'" width="520px">
<el-form :model="form" label-width="96px">
<el-form-item label="所属院系">
<el-select v-model="form.schoolFacultyId" placeholder="请选择院系" style="width: 100%">
<el-option v-for="faculty in faculties" :key="faculty.schoolFacultyId" :label="faculty.schoolFacultyName" :value="faculty.schoolFacultyId" />
</el-select>
</el-form-item>
<el-form-item label="专业名称"><el-input v-model="form.schoolMajorName" /></el-form-item>
<el-form-item label="联系人"><el-input v-model="form.schoolMajorContact" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.schoolMajorContactTel" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addAdminMajor, deleteAdminMajor, getAdminFaculties, getAdminMajors, updateAdminMajor } from "@/api/schoolAdmin";
const query = reactive({ index: 1, size: 10, schoolMajorName: "", schoolFacultyId: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const faculties = ref([]);
const item = (row) => ({ ...(row.schoolMajor || row), schoolMajorId: row.schoolMajorId || row.schoolMajor?.schoolMajorId });
const loadFaculties = async () => {
const res = await getAdminFaculties({ index: 1, size: 1000 });
faculties.value = (res.data?.list || []).map((row) => row.schoolFaculty || row);
};
const load = async () => {
loading.value = true;
try {
const res = await getAdminMajors(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.schoolMajorName = "";
query.schoolFacultyId = "";
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...item(row) };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.schoolMajorId) {
await updateAdminMajor(form.value.schoolMajorId, form.value);
} else {
await addAdminMajor(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该专业?", "提示", { type: "warning" });
await deleteAdminMajor(item(row).schoolMajorId);
ElMessage.success("删除成功");
load();
};
onMounted(() => {
loadFaculties();
load();
});
</script>

@ -0,0 +1,85 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Organization</div>
<h1 class="admin-title">学校管理</h1>
<p class="admin-subtitle">维护当前学校的基础信息,所有变更只作用于本校范围。</p>
</section>
<section class="admin-panel">
<el-form :model="form" label-width="110px" class="admin-form">
<el-row :gutter="18">
<el-col :span="12">
<el-form-item label="学校名称">
<el-input v-model="form.schoolName" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="院校层次">
<el-input v-model="form.schoolGrade" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属省份">
<el-input v-model="form.schoolProvince" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属城市">
<el-input v-model="form.schoolCity" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系人">
<el-input v-model="form.schoolContacts" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话">
<el-input v-model="form.schoolContactsMobile" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系邮箱">
<el-input v-model="form.schoolContactsEmail" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="授权码">
<el-input v-model="form.schoolAuthCode" disabled />
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
</el-form-item>
</el-form>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage } from "element-plus";
import { getAdminSchool, updateAdminSchool } from "@/api/schoolAdmin";
const form = ref({});
const saving = ref(false);
const load = async () => {
const res = await getAdminSchool();
form.value = res.data || {};
};
const save = async () => {
saving.value = true;
try {
await updateAdminSchool(form.value);
ElMessage.success("保存成功");
await load();
} finally {
saving.value = false;
}
};
onMounted(load);
</script>

@ -0,0 +1,124 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Student</div>
<h1 class="admin-title">学生管理</h1>
<p class="admin-subtitle">管理本校学生账号,学生数据按班级归档并限制在当前学校。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.name" clearable placeholder="学生姓名" class="filter-input" @keyup.enter="load" />
<el-input v-model="query.username" clearable placeholder="学号" class="filter-input" @keyup.enter="load" />
<el-select v-model="query.schoolClassId" clearable placeholder="所属班级" class="filter-input">
<el-option v-for="klass in classes" :key="klass.schoolClassId" :label="klass.className" :value="klass.schoolClassId" />
</el-select>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="姓名" prop="name" min-width="120" />
<el-table-column label="学号" prop="username" min-width="140" />
<el-table-column label="班级" min-width="160">
<template #default="{ row }">{{ className(row.schoolClassId) }}</template>
</el-table-column>
<el-table-column label="手机" prop="phone" min-width="140" />
<el-table-column label="邮箱" prop="email" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pager" layout="total, prev, pager, next, sizes" :total="total" v-model:current-page="query.index" v-model:page-size="query.size" @current-change="load" @size-change="load" />
<el-dialog v-model="dialogVisible" :title="form.userId ? '编辑学生' : '新增学生'" width="520px">
<el-form :model="form" label-width="86px">
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="学号"><el-input v-model="form.username" :disabled="Boolean(form.userId)" /></el-form-item>
<el-form-item label="班级">
<el-select v-model="form.schoolClassId" placeholder="请选择班级" style="width: 100%">
<el-option v-for="klass in classes" :key="klass.schoolClassId" :label="klass.className" :value="klass.schoolClassId" />
</el-select>
</el-form-item>
<el-form-item label="性别"><el-input v-model="form.sex" /></el-form-item>
<el-form-item label="手机"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addAdminStudent, deleteAdminStudent, getAdminClasses, getAdminStudents, updateAdminStudent } from "@/api/schoolAdmin";
const query = reactive({ index: 1, size: 10, name: "", username: "", schoolClassId: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const classes = ref([]);
const className = (id) => classes.value.find((klass) => klass.schoolClassId === id)?.className || "";
const loadClasses = async () => {
const res = await getAdminClasses({ index: 1, size: 1000 });
classes.value = (res.data?.list || []).map((row) => row.schoolClass || row);
};
const load = async () => {
loading.value = true;
try {
const res = await getAdminStudents(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.name = "";
query.username = "";
query.schoolClassId = "";
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...row };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.userId) {
await updateAdminStudent(form.value.userId, form.value);
} else {
await addAdminStudent(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该学生?", "提示", { type: "warning" });
await deleteAdminStudent(row.userId);
ElMessage.success("删除成功");
load();
};
onMounted(() => {
loadClasses();
load();
});
</script>

@ -0,0 +1,104 @@
<template>
<div class="school-admin-workbench">
<section class="admin-hero">
<div class="admin-badge">School Admin · Teacher</div>
<h1 class="admin-title">教师管理</h1>
<p class="admin-subtitle">管理本校教师账号,后端会固定教师角色并限制在当前学校。</p>
</section>
<section class="admin-panel">
<div class="toolbar">
<el-input v-model="query.name" clearable placeholder="教师姓名" class="filter-input" @keyup.enter="load" />
<el-input v-model="query.username" clearable placeholder="工号" class="filter-input" @keyup.enter="load" />
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="openCreate">新增</el-button>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column label="姓名" prop="name" min-width="120" />
<el-table-column label="工号" prop="username" min-width="140" />
<el-table-column label="性别" prop="sex" width="90" />
<el-table-column label="手机" prop="phone" min-width="140" />
<el-table-column label="邮箱" prop="email" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination class="pager" layout="total, prev, pager, next, sizes" :total="total" v-model:current-page="query.index" v-model:page-size="query.size" @current-change="load" @size-change="load" />
<el-dialog v-model="dialogVisible" :title="form.userId ? '编辑教师' : '新增教师'" width="520px">
<el-form :model="form" label-width="86px">
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
<el-form-item label="工号"><el-input v-model="form.username" :disabled="Boolean(form.userId)" /></el-form-item>
<el-form-item label="性别"><el-input v-model="form.sex" /></el-form-item>
<el-form-item label="手机"><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit">保存</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup>
import "@/views/schoolAdmin/admin-theme.scss";
import { ElMessage, ElMessageBox } from "element-plus";
import { addAdminTeacher, deleteAdminTeacher, getAdminTeachers, updateAdminTeacher } from "@/api/schoolAdmin";
const query = reactive({ index: 1, size: 10, name: "", username: "" });
const rows = ref([]);
const total = ref(0);
const loading = ref(false);
const dialogVisible = ref(false);
const form = ref({});
const load = async () => {
loading.value = true;
try {
const res = await getAdminTeachers(query);
rows.value = res.data?.list || [];
total.value = res.data?.total || 0;
} finally {
loading.value = false;
}
};
const reset = () => {
query.index = 1;
query.name = "";
query.username = "";
load();
};
const openCreate = () => {
form.value = {};
dialogVisible.value = true;
};
const openEdit = (row) => {
form.value = { ...row };
dialogVisible.value = true;
};
const submit = async () => {
if (form.value.userId) {
await updateAdminTeacher(form.value.userId, form.value);
} else {
await addAdminTeacher(form.value);
}
ElMessage.success("保存成功");
dialogVisible.value = false;
load();
};
const remove = async (row) => {
await ElMessageBox.confirm("确认删除该教师?", "提示", { type: "warning" });
await deleteAdminTeacher(row.userId);
ElMessage.success("删除成功");
load();
};
onMounted(load);
</script>
Loading…
Cancel
Save