Archived
forked from admin/userbase
This commit is contained in:
+39
-8
@@ -10,6 +10,7 @@ import Apis from '@/api/index'
|
||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||
import { UserRoleEnum } from "./types";
|
||||
import { isRouteAllowed } from "@/router";
|
||||
const store = useDefaultStore();
|
||||
const router = useRouter();
|
||||
const { $bus } = useCurrentInstance();
|
||||
@@ -53,10 +54,9 @@ const layoutInit = (info: any, modList: any[], appList: Array<any>, baseHost: st
|
||||
};
|
||||
|
||||
/**
|
||||
* 角色判定
|
||||
* 现基于 GetUserInfo 返回的 user.type 判定角色:
|
||||
* type=1 → SuperAdmin,type=2 → School(不再有 Teacher 角色)
|
||||
* 同时写入 currentRole 供下游 Utils.getCurrentRoleInfo 读取(保持兼容)
|
||||
* 角色判定(降级兜底):GetUserScope 失败时按 GetUserInfo 的 user.type 推断——
|
||||
* type=1 → SuperAdmin,其余 → School。同时写入 currentRole 供下游
|
||||
* Utils.getCurrentRoleInfo 与请求头注入读取(保持兼容;正常路径以 GetUserScope 为准)。
|
||||
*/
|
||||
const setCurrentRole = (user: any) => {
|
||||
const userId = user?.id ?? null;
|
||||
@@ -87,12 +87,43 @@ onMounted(async () => {
|
||||
try {
|
||||
var user: any = await alovaInstance.Request(Apis.Base.getUserInfo());
|
||||
|
||||
// 系统字典 & 模块列表(模块当前写死,后续接入接口)
|
||||
await store.fetchSysDict();
|
||||
await store.fetchModules();
|
||||
|
||||
currentUser.value = user;
|
||||
|
||||
// 系统字典
|
||||
await store.fetchSysDict();
|
||||
|
||||
// 角色判定:优先 GetUserScope(后端单点判定四级访问级别,前端不重复实现规则),
|
||||
// 失败时降级走 setCurrentRole 的 type 推断
|
||||
setCurrentRole(user);
|
||||
try {
|
||||
const scope: any = await alovaInstance.Request(Apis.Base.getUserScope());
|
||||
if (scope && scope.level != null) {
|
||||
store.setUserRole(scope.level);
|
||||
store.setUserScope({
|
||||
bureauGroupId: scope.bureauGroupId ?? null,
|
||||
schoolGroupIds: scope.schoolGroupIds ?? null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("获取访问范围失败,按用户类型降级判定角色", e);
|
||||
}
|
||||
|
||||
// NONE 级:整体「暂无权限」,不加载菜单(接口层已被后端 NONE 拦截器 403 双挡)
|
||||
store.setRoleLoaded(true);
|
||||
if (store.userRole === UserRoleEnum.None) {
|
||||
isNoAuthority.value = true;
|
||||
layoutInit(user, [], [], baseHost, ssoHost);
|
||||
isLogin.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次导航早于角色落定被守卫放行:此处复核当前路由(直接敲 URL 场景),越权重定向 /no-auth
|
||||
if (!isRouteAllowed(router.currentRoute.value, store.userRole)) {
|
||||
router.replace("/no-auth");
|
||||
}
|
||||
|
||||
// 模块列表(按当前级别过滤菜单权限矩阵)
|
||||
await store.fetchModules();
|
||||
layoutInit(user, store.modules, store.modules, baseHost, ssoHost);
|
||||
|
||||
// 登录后直接跳转到左侧菜单第一个可用入口
|
||||
|
||||
@@ -96,7 +96,9 @@ export default {
|
||||
'Base.RedirectTo': ['GET', '/api/base/RedirectTo/{id}'],
|
||||
'Base.manualHashCacheDemo': ['GET', '/api/base/ManualHashCacheDemo'],
|
||||
'Base.getUserInfo': ['GET', '/api/base/GetUserInfo'],
|
||||
'Base.getUserScope': ['GET', '/api/base/GetUserScope'],
|
||||
'Base.getSysDict': ['GET', '/api/base/GetSysDict'],
|
||||
'AppSettings.schoolList': ['GET', '/api/app/SchoolList'],
|
||||
'AppRole.roleUsers': ['GET', '/api/approle/RoleUsers'],
|
||||
'AppRole.roleModuleTree': ['GET', '/api/approle/RoleModuleTree'],
|
||||
'AppRole.roleList': ['GET', '/api/approle/RoleList'],
|
||||
|
||||
Vendored
+34
@@ -2758,6 +2758,23 @@ declare global {
|
||||
getUserInfo<Config extends Alova2MethodConfig<object>>(
|
||||
config?: Config
|
||||
): Alova2Method<object, 'Base.getUserInfo', Config>;
|
||||
/**
|
||||
* ---
|
||||
*
|
||||
* [GET]
|
||||
*
|
||||
* **path:** /api/base/GetUserScope
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* **Response**
|
||||
* ```ts
|
||||
* type Response = { level: number, bureauGroupId?: number, schoolGroupIds?: number[] }
|
||||
* ```
|
||||
*/
|
||||
getUserScope<Config extends Alova2MethodConfig<object>>(
|
||||
config?: Config
|
||||
): Alova2Method<object, 'Base.getUserScope', Config>;
|
||||
/**
|
||||
* ---
|
||||
*
|
||||
@@ -3920,6 +3937,23 @@ declare global {
|
||||
>(
|
||||
config: Config
|
||||
): Alova2Method<BasePageResultApplicationInfoVo, 'AppSettings.appList', Config>;
|
||||
/**
|
||||
* ---
|
||||
*
|
||||
* [GET]
|
||||
*
|
||||
* **path:** /api/app/SchoolList
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* **Response**
|
||||
* ```ts
|
||||
* type Response = Array<{ id: number, name: string }>
|
||||
* ```
|
||||
*/
|
||||
schoolList<Config extends Alova2MethodConfig<Array<any>>>(
|
||||
config?: Config
|
||||
): Alova2Method<Array<any>, 'AppSettings.schoolList', Config>;
|
||||
/**
|
||||
* ---
|
||||
*
|
||||
|
||||
+46
-22
@@ -1,26 +1,23 @@
|
||||
import type { IModule } from "@/store";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
|
||||
/**
|
||||
* 左侧菜单数据(系统管理功能分组)
|
||||
* 左侧菜单数据(系统管理功能分组)+ 菜单权限矩阵(管理端权限分级改造 §4.2)。
|
||||
* ----------------------------------------------------------------
|
||||
* 当前直接 Promise.resolve 返回写死数据;后续接入真实接口时,
|
||||
* 只需把 `return Promise.resolve(menu)` 替换为
|
||||
* `return alovaInstance.Request(Apis.xxx.getMenu())`
|
||||
* 即可无缝对接,调用方(store.fetchModules)无需任何改动。
|
||||
* 每个节点带 roles 元数据(可访问级别集合),getMenuList(userRole) 按级别过滤,
|
||||
* children 过滤后为空的父目录整体隐藏。当前数据写死;后续接入真实接口时,
|
||||
* 只需把 `return Promise.resolve(...)` 替换为接口调用后 filterMenuByRole 即可。
|
||||
*
|
||||
* 分组结构:
|
||||
* 1. 组,用户管理 —— 顶层独立选项卡(单页)
|
||||
* 2. 应用管理 —— 顶层独立选项卡(= 应用列表主页 /app-manage/list);
|
||||
* 页内按钮跳转「应用类别管理」子页(/app-manage/category,不在菜单里,
|
||||
* 由 routes.ts 的 meta.parentId=1102 关联,面包屑「应用管理>应用类别管理」)
|
||||
* 3. 权限管理 —— 父目录:应用开通 / 组授权 / 权限一览
|
||||
* 4. 日志管理 —— 父目录:接口日志 / 系统日志
|
||||
* 1. 组,用户管理 —— S+B+C
|
||||
* 2. 应用管理 —— S+C(局端不开放:局端的应用动作是「开通」,应用维护是平台级)
|
||||
* 3. 权限管理 —— 父目录:应用开通(S+B) / 组授权 / 角色授权 / 权限一览(S+B+C)
|
||||
* 4. 日志管理 —— 仅超管(父目录)
|
||||
*
|
||||
* 每个节点带唯一 id;子节点的 pid 由 layout.vue 的 flaten() 扁平化时
|
||||
* 依据父节点 id 自动推导,用于「高亮自身 + 面包屑展示父级」联动。
|
||||
* 菜单项不再渲染 icon。
|
||||
*/
|
||||
export function getMenuList(): Promise<IModule[]> {
|
||||
export function getMenuList(userRole?: UserRoleEnum): Promise<IModule[]> {
|
||||
const menu: IModule[] = [
|
||||
// 1. 组,用户管理(顶层独立选项卡)
|
||||
{
|
||||
@@ -28,6 +25,7 @@ export function getMenuList(): Promise<IModule[]> {
|
||||
modId: 1101,
|
||||
name: "组,用户管理",
|
||||
page: "/group-user",
|
||||
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School],
|
||||
},
|
||||
// 2. 应用管理(顶层独立选项卡,= 应用列表主页;页内按钮跳转应用类别管理子页)
|
||||
{
|
||||
@@ -35,6 +33,7 @@ export function getMenuList(): Promise<IModule[]> {
|
||||
modId: 1102,
|
||||
name: "应用管理",
|
||||
page: "/app-manage/list",
|
||||
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.School],
|
||||
},
|
||||
// 3. 权限管理(父目录)
|
||||
{
|
||||
@@ -43,11 +42,12 @@ export function getMenuList(): Promise<IModule[]> {
|
||||
name: "权限管理",
|
||||
page: "",
|
||||
childState: true,
|
||||
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School],
|
||||
children: [
|
||||
{ id: 41, modId: 1103, name: "应用开通", page: "/app-open" },
|
||||
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth" },
|
||||
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list" },
|
||||
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view" },
|
||||
{ id: 41, modId: 1103, name: "应用开通", page: "/app-open", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau] },
|
||||
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||
],
|
||||
},
|
||||
// 4. 日志管理(父目录)
|
||||
@@ -57,13 +57,37 @@ export function getMenuList(): Promise<IModule[]> {
|
||||
name: "日志管理",
|
||||
page: "",
|
||||
childState: true,
|
||||
roles: [UserRoleEnum.SuperAdmin],
|
||||
children: [
|
||||
{ id: 53, modId: 1111, name: "操作日志", page: "/oplog" },
|
||||
{ id: 56, modId: 1114, name: "开放接口日志", page: "/oapilog" },
|
||||
{ id: 54, modId: 1112, name: "推送日志", page: "/push-log" },
|
||||
{ id: 55, modId: 1113, name: "测试推送", page: "/push-test" },
|
||||
{ id: 53, modId: 1111, name: "操作日志", page: "/oplog", roles: [UserRoleEnum.SuperAdmin] },
|
||||
{ id: 56, modId: 1114, name: "开放接口日志", page: "/oapilog", roles: [UserRoleEnum.SuperAdmin] },
|
||||
{ id: 54, modId: 1112, name: "推送日志", page: "/push-log", roles: [UserRoleEnum.SuperAdmin] },
|
||||
{ id: 55, modId: 1113, name: "测试推送", page: "/push-test", roles: [UserRoleEnum.SuperAdmin] },
|
||||
],
|
||||
},
|
||||
];
|
||||
return Promise.resolve(menu);
|
||||
return Promise.resolve(filterMenuByRole(menu, userRole));
|
||||
}
|
||||
|
||||
/** 按级别过滤菜单:roles 未声明的节点视为全部级别可见;children 过滤后为空的父目录整体隐藏 */
|
||||
export function filterMenuByRole(menu: IModule[], userRole?: UserRoleEnum): IModule[] {
|
||||
if (userRole == null) {
|
||||
return menu;
|
||||
}
|
||||
const result: IModule[] = [];
|
||||
for (const item of menu) {
|
||||
const roles = (item as any).roles as UserRoleEnum[] | undefined;
|
||||
if (roles && !roles.includes(userRole)) {
|
||||
continue;
|
||||
}
|
||||
const children = item.children
|
||||
? filterMenuByRole(item.children, userRole)
|
||||
: undefined;
|
||||
if (item.childState && (!children || children.length === 0)) {
|
||||
// 父目录:可见但全部子项被过滤 → 整体隐藏
|
||||
continue;
|
||||
}
|
||||
result.push(children ? { ...item, children } : item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+37
-2
@@ -1,9 +1,44 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from "vue-router";
|
||||
import routes from "./routes";
|
||||
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
export default router;
|
||||
|
||||
/**
|
||||
* 当前角色是否可访问目标路由(meta.roles 未声明 = 登录即可访问)。
|
||||
* 供路由守卫与 App.vue 角色落定后的复核共用。
|
||||
*/
|
||||
export function isRouteAllowed(
|
||||
route: RouteLocationNormalizedLoaded | RouteRecordRaw,
|
||||
role: UserRoleEnum
|
||||
): boolean {
|
||||
const roles = (route.meta?.roles ?? null) as UserRoleEnum[] | null;
|
||||
return !roles || roles.length === 0 || roles.includes(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由守卫(防止直接敲 URL 绕过菜单):
|
||||
* - 角色未落定(roleLoaded=false,首次导航早于 App.vue 的 GetUserScope)时放行,
|
||||
* 由 App.vue 在角色落定后调用本守卫的复核逻辑重定向;
|
||||
* - 角色已落定后按 meta.roles 校验,不满足重定向 /no-auth。
|
||||
*/
|
||||
router.beforeEach(to => {
|
||||
if (to.path === "/no-auth") {
|
||||
return true;
|
||||
}
|
||||
const store = useDefaultStore();
|
||||
if (!store.roleLoaded) {
|
||||
return true;
|
||||
}
|
||||
if (!isRouteAllowed(to, store.userRole)) {
|
||||
return { path: "/no-auth", replace: true };
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
+28
-14
@@ -1,4 +1,5 @@
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
import GroupUser from "@/views/group-user/index.vue";
|
||||
import AppList from "@/views/app/AppList.vue";
|
||||
import AppCategory from "@/views/app/AppCategory.vue";
|
||||
@@ -12,41 +13,54 @@ import OperationLogList from "@/views/oplog/OperationLogList.vue";
|
||||
import OapiLogList from "@/views/oapilog/OapiLogList.vue";
|
||||
import PushLog from "@/views/push/PushLog.vue";
|
||||
import PushTest from "@/views/push/PushTest.vue";
|
||||
import NoAuth from "@/views/NoAuth.vue";
|
||||
|
||||
// 管理端四级访问控制:meta.roles 为空/未声明 = 登录即可访问
|
||||
const ALL_ADMIN = [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School];
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/",
|
||||
redirect: "/group-user",
|
||||
},
|
||||
// ===== 无权限页(路由守卫重定向目标)=====
|
||||
{
|
||||
path: "/no-auth",
|
||||
name: "NoAuth",
|
||||
component: NoAuth,
|
||||
meta: { title: "暂无权限" },
|
||||
},
|
||||
// ===== 开发组(组/用户管理,多页嵌套目录)=====
|
||||
{
|
||||
path: "/group-user",
|
||||
name: "GroupUser",
|
||||
component: GroupUser,
|
||||
meta: { title: "组,用户管理", icon: "icon-list", modId: 1101 },
|
||||
meta: { title: "组,用户管理", icon: "icon-list", modId: 1101, roles: ALL_ADMIN },
|
||||
},
|
||||
// ===== 应用管理(单选项卡)=====
|
||||
// 应用管理主页(= 应用列表),菜单顶层「应用管理」指向此路由(modId 1102)
|
||||
// 局端不开放应用管理(局端的应用动作是「开通」)
|
||||
{
|
||||
path: "/app-manage/list",
|
||||
name: "AppList",
|
||||
component: AppList,
|
||||
meta: { title: "应用管理", icon: "icon-list", modId: 1102, keepAlive: true },
|
||||
meta: { title: "应用管理", icon: "icon-list", modId: 1102, keepAlive: true, roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.School] },
|
||||
},
|
||||
// 应用类型管理(深层子页,不在菜单里):meta.parentId=1102 关联父菜单「应用管理」,
|
||||
// 由 layout.vue 的 routeChange 推导面包屑「应用管理>应用类型管理」并高亮父菜单
|
||||
// 校管可:类别改名/排序(写覆盖表);新增/删除类别按钮由页面按级别隐藏
|
||||
{
|
||||
path: "/app-manage/category",
|
||||
name: "AppCategory",
|
||||
component: AppCategory,
|
||||
meta: { title: "应用类型管理", modId: 11022, parentId: 1102 },
|
||||
meta: { title: "应用类型管理", modId: 11022, parentId: 1102, roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.School] },
|
||||
},
|
||||
// 模块管理(深层子页,不在菜单里):meta.parentId=1102 关联父菜单「应用管理」
|
||||
// 模块管理(深层子页,不在菜单里):仅超管
|
||||
{
|
||||
path: "/app-manage/module",
|
||||
name: "ModuleManage",
|
||||
component: ModuleManage,
|
||||
meta: { title: "模块管理", modId: 11023, parentId: 1102 },
|
||||
meta: { title: "模块管理", modId: 11023, parentId: 1102, roles: [UserRoleEnum.SuperAdmin] },
|
||||
},
|
||||
// /app-manage 兜底重定向到主页
|
||||
{
|
||||
@@ -59,7 +73,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "/role-authz/list",
|
||||
name: "RoleAuthzList",
|
||||
component: RoleAuthzList,
|
||||
meta: { title: "角色授权", icon: "icon-list", modId: 1110, keepAlive: true },
|
||||
meta: { title: "角色授权", icon: "icon-list", modId: 1110, keepAlive: true, roles: ALL_ADMIN },
|
||||
},
|
||||
// 角色管理(深层子页,不在菜单里):meta.parentId=1110 关联父菜单「角色授权」,
|
||||
// 由 layout.vue 推导面包屑「角色授权>角色管理」并高亮父菜单(与 /app-manage/category 范式一致)
|
||||
@@ -67,7 +81,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "/role-authz/manage",
|
||||
name: "RoleManage",
|
||||
component: RoleManage,
|
||||
meta: { title: "角色管理", modId: 11101, parentId: 1110 },
|
||||
meta: { title: "角色管理", modId: 11101, parentId: 1110, roles: ALL_ADMIN },
|
||||
},
|
||||
// /role-authz 兜底重定向到主页
|
||||
{
|
||||
@@ -78,45 +92,45 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "/app-open",
|
||||
name: "AppOpen",
|
||||
component: AppOpen,
|
||||
meta: { title: "应用开通", icon: "icon-list", modId: 1103 },
|
||||
meta: { title: "应用开通", icon: "icon-list", modId: 1103, roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau] },
|
||||
},
|
||||
{
|
||||
path: "/group-auth",
|
||||
name: "GroupAuth",
|
||||
component: GroupAuth,
|
||||
meta: { title: "组授权", icon: "icon-list", modId: 1104 },
|
||||
meta: { title: "组授权", icon: "icon-list", modId: 1104, roles: ALL_ADMIN },
|
||||
},
|
||||
{
|
||||
path: "/permission-view",
|
||||
name: "PermissionView",
|
||||
component: PermissionView,
|
||||
meta: { title: "权限一览", icon: "icon-list", modId: 1105 },
|
||||
meta: { title: "权限一览", icon: "icon-list", modId: 1105, roles: ALL_ADMIN },
|
||||
},
|
||||
{
|
||||
path: "/push-log",
|
||||
name: "PushLog",
|
||||
component: PushLog,
|
||||
meta: { title: "推送日志", icon: "icon-list", modId: 1112, keepAlive: true },
|
||||
meta: { title: "推送日志", icon: "icon-list", modId: 1112, keepAlive: true, roles: [UserRoleEnum.SuperAdmin] },
|
||||
},
|
||||
{
|
||||
path: "/push-test",
|
||||
name: "PushTest",
|
||||
component: PushTest,
|
||||
meta: { title: "测试推送", icon: "icon-list", modId: 1113 },
|
||||
meta: { title: "测试推送", icon: "icon-list", modId: 1113, roles: [UserRoleEnum.SuperAdmin] },
|
||||
},
|
||||
// ===== 操作日志 =====
|
||||
{
|
||||
path: "/oplog",
|
||||
name: "OperationLogList",
|
||||
component: OperationLogList,
|
||||
meta: { title: "操作日志", icon: "icon-list", modId: 1111, keepAlive: true },
|
||||
meta: { title: "操作日志", icon: "icon-list", modId: 1111, keepAlive: true, roles: [UserRoleEnum.SuperAdmin] },
|
||||
},
|
||||
// ===== 开放接口日志 =====
|
||||
{
|
||||
path: "/oapilog",
|
||||
name: "OapiLogList",
|
||||
component: OapiLogList,
|
||||
meta: { title: "开放接口日志", icon: "icon-list", modId: 1114, keepAlive: true },
|
||||
meta: { title: "开放接口日志", icon: "icon-list", modId: 1114, keepAlive: true, roles: [UserRoleEnum.SuperAdmin] },
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+14
-2
@@ -22,12 +22,16 @@ export interface IModule {
|
||||
export interface IdefaultStore {
|
||||
curStatsDate: Date;
|
||||
userRole: UserRoleEnum;
|
||||
/** 管理端访问范围(GetUserScope 返回,后端单点判定):BUREAU 区域组 / SCHOOL 可管理学校组 */
|
||||
userScope: { bureauGroupId?: number | null; schoolGroupIds?: number[] | null };
|
||||
lenLevel: Array<SchLenLevel>;
|
||||
subjects: Array<Subject>;
|
||||
/** 系统字典(GetSysDict 接口返回) */
|
||||
sysDict: Record<string, any>;
|
||||
/** 模块列表(当前写死,后续接入接口) */
|
||||
modules: Array<IModule>;
|
||||
/** 角色是否已落定(App.vue GetUserScope/降级判定完成后置 true,路由守卫依赖) */
|
||||
roleLoaded: boolean;
|
||||
}
|
||||
|
||||
export const useDefaultStore = defineStore("defaultStore", {
|
||||
@@ -35,10 +39,12 @@ export const useDefaultStore = defineStore("defaultStore", {
|
||||
return {
|
||||
curStatsDate: new Date(),
|
||||
userRole: UserRoleEnum.School,
|
||||
userScope: { bureauGroupId: null, schoolGroupIds: null },
|
||||
lenLevel: [],
|
||||
subjects: [],
|
||||
sysDict: {},
|
||||
modules: [],
|
||||
roleLoaded: false,
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
@@ -48,6 +54,12 @@ export const useDefaultStore = defineStore("defaultStore", {
|
||||
setUserRole(role: UserRoleEnum) {
|
||||
this.userRole = role;
|
||||
},
|
||||
setUserScope(scope: { bureauGroupId?: number | null; schoolGroupIds?: number[] | null }) {
|
||||
this.userScope = scope || { bureauGroupId: null, schoolGroupIds: null };
|
||||
},
|
||||
setRoleLoaded(loaded: boolean) {
|
||||
this.roleLoaded = loaded;
|
||||
},
|
||||
setSchLenLevel(len: Array<SchLenLevel>) {
|
||||
this.lenLevel = len;
|
||||
},
|
||||
@@ -61,9 +73,9 @@ export const useDefaultStore = defineStore("defaultStore", {
|
||||
this.sysDict = {};
|
||||
}
|
||||
},
|
||||
/** 获取模块列表(数据来源见 src/assets/config/menu.ts,后续接入接口无缝替换) */
|
||||
/** 获取模块列表(数据源 src/assets/config/menu.ts,按当前级别过滤菜单权限矩阵) */
|
||||
async fetchModules() {
|
||||
this.modules = await getMenuList();
|
||||
this.modules = await getMenuList(this.userRole);
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -7,9 +7,15 @@ export enum OperatorEnum {
|
||||
sel,
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端访问级别(与后端 AccessLevelEnum / GetUserScope.level 对齐,后端单点判定)。
|
||||
* 注意:School 由旧值 2 改为 3(原枚举值无消费方依赖具体数值,安全重排)。
|
||||
*/
|
||||
export enum UserRoleEnum {
|
||||
SuperAdmin = 1,
|
||||
Bureau,
|
||||
School,
|
||||
None,
|
||||
}
|
||||
|
||||
//学段
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { useDefaultStore } from "@/store";
|
||||
import { UserRoleEnum } from "@/types";
|
||||
|
||||
/**
|
||||
* 应用管理页学校上下文(管理端权限分级改造 §4.3)。<br>
|
||||
* SCHOOL 模式专属:多校管理员(schoolGroupIds.length > 1)页面顶部出现「学校」下拉,
|
||||
* currentSchoolId 随接口下发给 AppList/AppSortList/AppSort/EditApp/EditType/AppTypeSort,
|
||||
* 覆盖只对所选学校生效;单校管理员不显示下拉(后端自动取唯一学校),
|
||||
* 超管/局端不进入本上下文(局端无应用管理菜单)。
|
||||
*/
|
||||
export function useAppSchoolScope() {
|
||||
const store = useDefaultStore();
|
||||
const isSchoolMode = computed(() => store.userRole === UserRoleEnum.School);
|
||||
const schoolGroupIds = computed<number[]>(() => store.userScope?.schoolGroupIds ?? []);
|
||||
const multiSchool = computed(() => isSchoolMode.value && schoolGroupIds.value.length > 1);
|
||||
|
||||
const schoolList = ref<{ id: number; name: string }[]>([]);
|
||||
// 默认不选中(currentSchoolId=undefined):查询走后端「多校并集」模式;
|
||||
// 写操作(编辑/排序)要求先选定学校,由页面用 requireSchool() 守卫
|
||||
const currentSchoolId = ref<number | undefined>(undefined);
|
||||
|
||||
/** 多校模式下写操作前置校验:未选学校时提示并阻断,返回 false */
|
||||
function requireSchool(): boolean {
|
||||
if (multiSchool.value && !currentSchoolId.value) {
|
||||
Message.warning("您管理多所学校,请先在顶部选择学校后再操作");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (multiSchool.value) {
|
||||
// 下拉数据源(SUPER/BUREAU 后端返回空数组,前端也不渲染下拉)
|
||||
Promise.resolve(Apis.AppSettings.schoolList({ cacheFor: null }))
|
||||
.then((list: any) => {
|
||||
schoolList.value = Array.isArray(list) ? list : [];
|
||||
})
|
||||
.catch(e => console.warn("获取可管理学校列表失败", e));
|
||||
}
|
||||
|
||||
return { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="no-auth-page">
|
||||
<h3>暂无权限</h3>
|
||||
<h6>如需查看,请联络管理员为您分配权限</h6>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: "NoAuth" });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.no-auth-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,13 @@
|
||||
<div class="main-wrapper">
|
||||
<div class="main-container">
|
||||
<Alert show-icon>设置说明:拖动排序,被使用的分类不能删除</Alert>
|
||||
<Button class="mb-5" type="primary" @click="addModuleShow">新增应用类型</Button>
|
||||
<!-- 多校管理员:学校切换(改名/排序仅对所选学校生效) -->
|
||||
<Select v-if="multiSchool" v-model="currentSchoolId" class="mb-5 school-select" transfer
|
||||
placeholder="全部学校(并集)" @on-change="fetchTypeList">
|
||||
<Option v-for="s in schoolList" :key="s.id" :value="s.id">{{ s.name }}</Option>
|
||||
</Select>
|
||||
<Button v-if="!isSchoolMode" class="mb-5" :class="{ 'ml-8': multiSchool }" type="primary"
|
||||
@click="addModuleShow">新增应用类型</Button>
|
||||
<draggable class="type_set-wrap fix" tag="ul" v-model="typeList" :group="'appType'" @change="typeSortSave"
|
||||
item-key="id">
|
||||
<template #item="{ element }">
|
||||
@@ -16,7 +22,7 @@
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
|
||||
<div class="action_delete" @click="removeSave(element)">
|
||||
<div class="action_delete" v-if="!isSchoolMode" @click="removeSave(element)">
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,16 +55,25 @@
|
||||
import draggable from 'vuedraggable'
|
||||
import { debounce } from "lodash";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
|
||||
|
||||
const { data: typeList, send: sendTypeList } = useRequest(
|
||||
Apis.AppSettings.appTypeList({
|
||||
transform: (res: any) => {
|
||||
return res ?? [];
|
||||
},
|
||||
cacheFor: null,
|
||||
}),
|
||||
{ initialData: [], immediate: true }
|
||||
)
|
||||
// 学校管理员上下文:仅改名/排序(写学校覆盖表);新增/删除类型仅超管(按钮隐藏 + 接口 requireSuper 双挡)
|
||||
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
|
||||
|
||||
const typeList = ref<any[]>([])
|
||||
|
||||
async function fetchTypeList() {
|
||||
try {
|
||||
const res: any = await Apis.AppSettings.appTypeList({
|
||||
params: { schoolId: currentSchoolId.value ?? undefined },
|
||||
cacheFor: null,
|
||||
});
|
||||
typeList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "获取应用类型失败");
|
||||
}
|
||||
}
|
||||
fetchTypeList()
|
||||
|
||||
const refFormData = useTemplateRef('formData');
|
||||
const editModal = ref(false);
|
||||
@@ -80,13 +95,22 @@ async function addModuleShow() {
|
||||
editModal.value = true;
|
||||
}
|
||||
|
||||
//拖动排序:提交当前顺序的 id 数组
|
||||
//拖动排序:提交当前顺序的 id 数组(SCHOOL 模式写学校覆盖表快照;多校须先选定学校)
|
||||
async function typeSortSave() {
|
||||
if (!requireSchool()) {
|
||||
// 未选学校:拒绝提交并回拉列表,撤销拖拽造成的本地顺序变化
|
||||
await fetchTypeList();
|
||||
return;
|
||||
}
|
||||
const ids = (typeList.value ?? []).map((s: any) => s.id);
|
||||
await Apis.AppSettings.appTypeSort({ data: ids });
|
||||
await Apis.AppSettings.appTypeSort({
|
||||
data: ids,
|
||||
params: currentSchoolId.value != null ? { schoolId: currentSchoolId.value } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async function showSetting(element: any) {
|
||||
if (!requireSchool()) return;
|
||||
currentId.value = element.id;
|
||||
form.value = {
|
||||
id: element.id,
|
||||
@@ -106,7 +130,7 @@ async function removeSave(element: any) {
|
||||
try {
|
||||
await Apis.AppSettings.deleteType({ pathParams: { id: element.id } });
|
||||
Message.success("操作成功!");
|
||||
await sendTypeList();
|
||||
await fetchTypeList();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -138,11 +162,13 @@ async function onRawEditSave() {
|
||||
id: currentId.value,
|
||||
name: form.value.name,
|
||||
},
|
||||
// SCHOOL 模式:改名写学校覆盖表(仅该校生效)
|
||||
params: currentSchoolId.value != null ? { schoolId: currentSchoolId.value } : undefined,
|
||||
});
|
||||
}
|
||||
Message.success("操作成功!");
|
||||
editModal.value = false;
|
||||
await sendTypeList();
|
||||
await fetchTypeList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
@@ -163,6 +189,10 @@ function onReset() {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.school-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.type_set-wrap {
|
||||
margin: 12px 0 16px;
|
||||
}
|
||||
|
||||
+28
-16
@@ -2,14 +2,14 @@
|
||||
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
|
||||
@on-visible-change="onVisibleChange">
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="app-form width-95">
|
||||
<!-- 第一行:应用名称 / 应用类型 -->
|
||||
<!-- 第一行:应用名称 / 应用类型(学校模式隐藏类型——类型归属平台主数据) -->
|
||||
<Row>
|
||||
<Col span="12">
|
||||
<FormItem label="应用名称:" prop="name">
|
||||
<Input v-model="form.name" placeholder="请输入应用名称" />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span="12">
|
||||
<Col span="12" v-if="!schoolMode">
|
||||
<FormItem label="应用类型:" prop="categoryId">
|
||||
<Select v-model="form.categoryId" transfer placeholder="请选择应用类型">
|
||||
<Option v-for="item in categoryList" :key="item.id" :value="item.id" :label="item.name" />
|
||||
@@ -26,8 +26,15 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第三行:appKey / appSecret(编辑模式才显示) -->
|
||||
<Row v-if="isEdit">
|
||||
<!-- 学校模式说明:仅 名称/描述/图标 可编辑,且仅对本校生效 -->
|
||||
<Row v-if="schoolMode">
|
||||
<Col span="24">
|
||||
<Alert show-icon>学校模式:此处修改仅对本校生效(应用别名),平台主数据与密钥/推送等配置不可修改。</Alert>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第三行:appKey / appSecret(编辑模式且非学校模式才显示) -->
|
||||
<Row v-if="isEdit && !schoolMode">
|
||||
<Col span="12">
|
||||
<FormItem label="appKey:">
|
||||
<Input :model-value="form.appKey?.toString()" readonly />
|
||||
@@ -43,8 +50,8 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第四行:应用前端类型(tag 多选) -->
|
||||
<Row>
|
||||
<!-- 第四行:应用前端类型(tag 多选;学校模式隐藏) -->
|
||||
<Row v-if="!schoolMode">
|
||||
<Col span="24">
|
||||
<FormItem label="应用前端类型:">
|
||||
<Select v-model="form.clients" multiple transfer placeholder="请选择前端类型" class="client-select">
|
||||
@@ -54,9 +61,9 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第五行:Pc应用地址 / Pc应用图标 -->
|
||||
<!-- 第五行:Pc应用地址 / Pc应用图标(学校模式仅保留 Pc应用图标) -->
|
||||
<Row>
|
||||
<Col span="12">
|
||||
<Col span="12" v-if="!schoolMode">
|
||||
<FormItem label="Pc应用地址:">
|
||||
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
|
||||
</FormItem>
|
||||
@@ -79,8 +86,8 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第六行:App接口地址 / App应用图标 -->
|
||||
<Row>
|
||||
<!-- 第六行:App接口地址 / App应用图标(学校模式隐藏) -->
|
||||
<Row v-if="!schoolMode">
|
||||
<Col span="12">
|
||||
<FormItem label="App接口地址:">
|
||||
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
|
||||
@@ -104,8 +111,8 @@
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 第七行:公众号应用地址 / 钉钉应用地址 -->
|
||||
<Row>
|
||||
<!-- 第七行:公众号应用地址 / 钉钉应用地址(学校模式隐藏) -->
|
||||
<Row v-if="!schoolMode">
|
||||
<Col span="12">
|
||||
<FormItem label="公众号应用地址:">
|
||||
<Input v-model="form.officialAccountUrl" placeholder="请输入公众号应用地址" />
|
||||
@@ -139,11 +146,14 @@ const props = withDefaults(defineProps<{
|
||||
categoryList?: any[];
|
||||
isEdit?: boolean;
|
||||
loading?: boolean;
|
||||
/** 学校管理员模式:仅 名称/描述/Pc图标 三字段可编辑(写学校覆盖表) */
|
||||
schoolMode?: boolean;
|
||||
}>(), {
|
||||
initForm: () => ({}),
|
||||
categoryList: () => [],
|
||||
isEdit: false,
|
||||
loading: false,
|
||||
schoolMode: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'reset-secret']);
|
||||
@@ -211,11 +221,13 @@ const clientOptions = [
|
||||
{ value: 32, label: '钉钉' },
|
||||
];
|
||||
|
||||
/* ---------- 校验规则 ---------- */
|
||||
const rules = {
|
||||
/* ---------- 校验规则(学校模式不校验 categoryId——类型字段不展示且不可改) ---------- */
|
||||
const rules = computed(() => ({
|
||||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
||||
categoryId: [{ required: true, type: 'number' as const, message: '请选择应用类型', trigger: 'change' }],
|
||||
};
|
||||
...(props.schoolMode ? {} : {
|
||||
categoryId: [{ required: true, type: 'number' as const, message: '请选择应用类型', trigger: 'change' }],
|
||||
}),
|
||||
}));
|
||||
|
||||
/* ---------- 提交 ---------- */
|
||||
const formRef = ref();
|
||||
|
||||
+55
-21
@@ -18,9 +18,15 @@
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<div class="search-wrap fix">
|
||||
<Button class="fl" type="primary" @click="handleAdd">新增应用</Button>
|
||||
<!-- 多校管理员:学校切换(覆盖仅对所选学校生效) -->
|
||||
<Select v-if="multiSchool" v-model="currentSchoolId" class="fl school-select" transfer
|
||||
placeholder="全部学校(并集)" @on-change="onSchoolChange">
|
||||
<Option v-for="s in schoolList" :key="s.id" :value="s.id">{{ s.name }}</Option>
|
||||
</Select>
|
||||
<Button v-if="!isSchoolMode" class="fl" :class="{ 'ml-8': multiSchool }" type="primary"
|
||||
@click="handleAdd">新增应用</Button>
|
||||
<Button class="fl ml-8" type="default" v-if="dataParams.categoryId != null"
|
||||
@click="sortModalVisible = true">排序管理</Button>
|
||||
@click="openSortModal">排序管理</Button>
|
||||
<Input class="fr" v-model="keyword" placeholder="输入应用名称或appKey筛选" search clearable suffix="ios-search"
|
||||
style="width:220px" @on-search="onSearch" @on-clear="onSearch" />
|
||||
</div>
|
||||
@@ -30,11 +36,11 @@
|
||||
|
||||
<!-- 容器组件:新增 / 编辑 -->
|
||||
<AddAppContainer ref="addRef" />
|
||||
<EditAppContainer ref="editRef" />
|
||||
<EditAppContainer ref="editRef" :schoolId="currentSchoolId" />
|
||||
|
||||
<!-- 排序管理 -->
|
||||
<AppSort v-model="sortModalVisible" :categoryId="dataParams.categoryId" :categoryName="currentCategoryName"
|
||||
@saved="onSortSaved" />
|
||||
:schoolId="currentSchoolId" @saved="onSortSaved" />
|
||||
|
||||
<!-- 推送设置 -->
|
||||
<PushSettingModal v-model="pushSettingVisible" :appId="currentApp.id" :appName="currentApp.name" />
|
||||
@@ -53,10 +59,14 @@ import PushSettingModal from "./PushSettingModal.vue";
|
||||
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
|
||||
import { OperatorEnum } from '@/types';
|
||||
import AppSort from './AppSort.vue';
|
||||
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
|
||||
|
||||
const router = useRouter();
|
||||
const { $utils } = useCurrentInstance();
|
||||
|
||||
// 学校管理员上下文:多校显示切换下拉,覆盖仅对所选学校生效(默认并集,写操作须先选定学校)
|
||||
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
|
||||
|
||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||
|
||||
const sortModalVisible = ref(false);
|
||||
@@ -79,29 +89,39 @@ function onCategoryClick(id: number | null) {
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
/* ---------------- 多校切换:刷新类型与列表 ---------------- */
|
||||
function onSchoolChange() {
|
||||
fetchTypeList();
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
/* ---------------- 输入框搜索:仅在触发搜索时提交 ---------------- */
|
||||
function onSearch() {
|
||||
dataParams.value.name = keyword.value.trim() || undefined;
|
||||
tableRef.value?.refreshData();
|
||||
}
|
||||
|
||||
/* ---------------- 左侧应用类型 ---------------- */
|
||||
const { data: appTypeList, send: fetchTypeList } = useRequest(
|
||||
Apis.AppSettings.appTypeList({
|
||||
transform: (res: any) => {
|
||||
const list: any[] = res ?? [];
|
||||
return list.slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||||
},
|
||||
cacheFor: null,
|
||||
}),
|
||||
{ initialData: [] }
|
||||
);
|
||||
/* ---------------- 左侧应用类型(SCHOOL 模式合并学校私有改名/排序;随学校切换刷新) ---------------- */
|
||||
const appTypeList = ref<any[]>([]);
|
||||
async function fetchTypeList() {
|
||||
try {
|
||||
const res: any = await Apis.AppSettings.appTypeList({
|
||||
params: { schoolId: currentSchoolId.value ?? undefined },
|
||||
cacheFor: null,
|
||||
});
|
||||
appTypeList.value = ((res ?? []) as any[]).slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "获取应用类型失败");
|
||||
}
|
||||
}
|
||||
fetchTypeList();
|
||||
|
||||
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
|
||||
return Apis.AppSettings.appList({
|
||||
params: {
|
||||
categoryId: dataParams.value.categoryId ?? undefined,
|
||||
name: dataParams.value.name || undefined,
|
||||
schoolId: currentSchoolId.value ?? undefined,
|
||||
pageIndex: params.pageIndex,
|
||||
pageSize: params.pageSize,
|
||||
},
|
||||
@@ -154,12 +174,14 @@ const columns = [
|
||||
align: 'center',
|
||||
width: 280,
|
||||
render: (h: any, { row }: any) => {
|
||||
return [
|
||||
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
|
||||
$utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)),
|
||||
$utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)),
|
||||
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
|
||||
];
|
||||
// SCHOOL 模式:仅编辑(名称/描述/图标,写学校覆盖表);模块管理/推送设置/删除全部隐藏
|
||||
const actions = [$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row))];
|
||||
if (!isSchoolMode.value) {
|
||||
actions.push($utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)));
|
||||
actions.push($utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)));
|
||||
actions.push($utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -196,11 +218,19 @@ function handleAdd() {
|
||||
const editRef = ref();
|
||||
|
||||
function handleEdit(row: any) {
|
||||
// 多校管理员:编辑写学校覆盖表,必须先选定学校(后端对模糊写目标直接拒绝)
|
||||
if (!requireSchool()) return;
|
||||
editRef.value?.open(row.id, row, appTypeList.value, () => {
|
||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 排序管理入口(多校须先选定学校) ---------------- */
|
||||
function openSortModal() {
|
||||
if (!requireSchool()) return;
|
||||
sortModalVisible.value = true;
|
||||
}
|
||||
|
||||
/* ---------------- 删除应用 ---------------- */
|
||||
async function handleDelete(row: any) {
|
||||
await MyConfirm({
|
||||
@@ -246,6 +276,10 @@ const goModuleManage = (row: any) => {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.school-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.tree-scroll {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
@@ -40,9 +40,12 @@ const props = withDefaults(
|
||||
modelValue: boolean;
|
||||
categoryId: number | null;
|
||||
categoryName: string;
|
||||
/** 多校管理员当前所选学校:排序快照仅对该校生效 */
|
||||
schoolId?: number | null;
|
||||
}>(),
|
||||
{
|
||||
categoryName: "",
|
||||
schoolId: null,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,7 +86,10 @@ async function fetchSortList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await Apis.AppSettings.appSortList({
|
||||
params: { categoryId: props.categoryId },
|
||||
params: {
|
||||
categoryId: props.categoryId,
|
||||
schoolId: props.schoolId ?? undefined,
|
||||
},
|
||||
cacheFor: null,
|
||||
});
|
||||
sortList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||||
@@ -109,7 +115,10 @@ async function handleSave() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const ids = sortList.value.map((item) => item.id);
|
||||
await Apis.AppSettings.appSort({ data: ids });
|
||||
await Apis.AppSettings.appSort({
|
||||
data: ids,
|
||||
params: props.schoolId != null ? { schoolId: props.schoolId } : undefined,
|
||||
});
|
||||
Message.success("排序保存成功!");
|
||||
hasChanged.value = false;
|
||||
modalVisible.value = false;
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
<template>
|
||||
<AppForm v-model="visible" :initForm="detailData" :categoryList="categoryList" :isEdit="true" :loading="submitting"
|
||||
@submit="handleSubmit" @cancel="visible = false" @reset-secret="handleResetSecret" />
|
||||
:schoolMode="isSchoolMode" @submit="handleSubmit" @cancel="visible = false" @reset-secret="handleResetSecret" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import AppForm from "./AppForm.vue";
|
||||
import MyConfirm from '@/components/MyConfirm'
|
||||
import { UserRoleEnum } from "@/types";
|
||||
import { useDefaultStore } from "@/store";
|
||||
|
||||
/* ---------- 对外暴露 ---------- */
|
||||
const visible = ref(false);
|
||||
const categoryList = ref<any[]>([]);
|
||||
|
||||
// 编辑模式:从 appList 中根据 id 取出详情回填(无独立详情接口)
|
||||
const store = useDefaultStore();
|
||||
const isSchoolMode = store.userRole === UserRoleEnum.School;
|
||||
|
||||
// 多校管理员当前所选学校(单校/超管为 undefined,后端按 scope 自动解析)
|
||||
const props = defineProps<{ schoolId?: number | null }>();
|
||||
|
||||
// 编辑模式:从 appList 中根据 id 取出详情回填(无独立详情接口;SCHOOL 模式列表已合并覆盖值)
|
||||
const detailData = ref<Record<string, any>>({});
|
||||
const currentId = ref<number | null>(null);
|
||||
|
||||
@@ -21,12 +29,16 @@ let onSuccessCallback: (() => void) | null = null;
|
||||
|
||||
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
||||
(dto) => Apis.AppSettings.editApp({ pathParams: { id: currentId.value }, data: dto }),
|
||||
(dto) => Apis.AppSettings.editApp({
|
||||
pathParams: { id: currentId.value },
|
||||
data: dto,
|
||||
params: props.schoolId != null ? { schoolId: props.schoolId } : undefined,
|
||||
}),
|
||||
{ initialForm: { id: null, name: '', categoryId: undefined, desc: '', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||
);
|
||||
|
||||
onSuccess(() => {
|
||||
Message.success('编辑应用成功!');
|
||||
Message.success(isSchoolMode ? '保存成功(仅对本校生效)!' : '编辑应用成功!');
|
||||
visible.value = false;
|
||||
onSuccessCallback?.();
|
||||
});
|
||||
@@ -54,23 +66,33 @@ function open(id: number, row: any, categories: any[], onSuccess?: () => void) {
|
||||
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
||||
async function handleSubmit(formData: Record<string, any>) {
|
||||
if (!currentId.value) return;
|
||||
form.value = {
|
||||
id: currentId.value,
|
||||
name: formData.name,
|
||||
categoryId: formData.categoryId,
|
||||
desc: formData.desc,
|
||||
clients: formData.clients,
|
||||
pcUrl: formData.pcUrl,
|
||||
pcIcon: formData.pcIcon,
|
||||
appUrl: formData.appUrl,
|
||||
appIcon: formData.appIcon,
|
||||
officialAccountUrl: formData.officialAccountUrl,
|
||||
dingtalkUrl: formData.dingtalkUrl,
|
||||
};
|
||||
if (isSchoolMode) {
|
||||
// 学校模式:仅 名称/描述/Pc图标 三字段写学校覆盖表,其余字段一律不提交
|
||||
form.value = {
|
||||
id: currentId.value,
|
||||
name: formData.name,
|
||||
desc: formData.desc,
|
||||
pcIcon: formData.pcIcon,
|
||||
};
|
||||
} else {
|
||||
form.value = {
|
||||
id: currentId.value,
|
||||
name: formData.name,
|
||||
categoryId: formData.categoryId,
|
||||
desc: formData.desc,
|
||||
clients: formData.clients,
|
||||
pcUrl: formData.pcUrl,
|
||||
pcIcon: formData.pcIcon,
|
||||
appUrl: formData.appUrl,
|
||||
appIcon: formData.appIcon,
|
||||
officialAccountUrl: formData.officialAccountUrl,
|
||||
dingtalkUrl: formData.dingtalkUrl,
|
||||
};
|
||||
}
|
||||
await sendForm();
|
||||
}
|
||||
|
||||
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await) ---------- */
|
||||
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await;学校模式入口已隐藏) ---------- */
|
||||
async function handleResetSecret() {
|
||||
if (!currentId.value) return;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user