Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1b16332e4 | ||
|
|
c7404b9d5e | ||
|
|
7e37f2ada6 | ||
|
|
9200fa2135 | ||
|
|
790f0e3db5 | ||
|
|
428ee56bd3 | ||
|
|
886cce1b9e | ||
|
|
73066b5ab3 | ||
|
|
9c04b5e9fc | ||
|
|
f5c1289681 |
@@ -1,7 +0,0 @@
|
|||||||
# 正式环境 - base
|
|
||||||
VITE_BASE_HOST=https://base.bestsch.com
|
|
||||||
VITE_SSO_HOST=https://sso.bestsch.com
|
|
||||||
VITE_SERVICE_HOST=https://base.bestsch.com/LearnCenter
|
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# 开发环境 (dev)
|
# 开发环境 (dev):仅 vite dev 加载(--mode dev)。值可自由指本地或线上地址。
|
||||||
|
# build 产物不读此文件(build 默认 production mode,只认 .env / .env.production,两者均不存在),
|
||||||
|
# 线上环境差异全部由 projman 的 env-config-{目标}.json 承担。
|
||||||
VITE_BASE_HOST=http://base.bestsch.com
|
VITE_BASE_HOST=http://base.bestsch.com
|
||||||
VITE_SSO_HOST=http://sso.bestsch.com
|
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
||||||
VITE_SERVICE_HOST=http://localhost:8060
|
VITE_SERVICE_HOST=http://localhost:8060
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# 正式环境 - ld
|
|
||||||
VITE_BASE_HOST=https://ld.bestsch.com
|
|
||||||
VITE_SSO_HOST=https://sso.bestsch.com
|
|
||||||
VITE_SERVICE_HOST=https://ld.bestsch.com/LearnCenter
|
|
||||||
VITE_ZIYUANKU_HOST=https://ld.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://ld.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# 正式环境 - 云海星通(部署在 base.yunhaixingtong.com/userbase)
|
|
||||||
VITE_BASE_HOST=http://base.yunhaixingtong.com
|
|
||||||
VITE_SSO_HOST=http://sso.yunhaixingtong.com
|
|
||||||
VITE_SERVICE_HOST=http://base.yunhaixingtong.com/userbase
|
|
||||||
VITE_ZIYUANKU_HOST=https://base.bestsch.com/BschZyk
|
|
||||||
VITE_ZHUANXIANG_HOST=https://base.bestsch.com/BschZncpd
|
|
||||||
VITE_IS_MOCK=false
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
//铃铛debug
|
|
||||||
window.conf = {
|
|
||||||
baseHost: "http://basedebug.bestsch.com",
|
|
||||||
ssoHost: "http://ssodebug.bestsch.com",
|
|
||||||
serviceHost: "http://basedebug.bestsch.com/QualityEvaluate",
|
|
||||||
};
|
|
||||||
+22
@@ -5,6 +5,28 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="../../favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- 运行时配置引导:在任何应用模块执行前同步拉取 /<应用>/<前端目标>/api/env-config
|
||||||
|
(projman 配置管理在线维护、双写 Nacos),替代 config/index.js 与生产构建环境变量。
|
||||||
|
必须同步:host.config.ts 在模块求值期就读 window.__APP_CONFIG__;失败/本地开发
|
||||||
|
(路径不足两段)则不设置:本地走 .env.dev(import.meta.env.VITE_*),线上走同域推导兜底。
|
||||||
|
注意:同步 XHR 不支持 timeout 属性(赋值会抛 InvalidAccessError),依赖内网快速失败 -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var seg = location.pathname.split("/").filter(Boolean);
|
||||||
|
if (seg.length < 2) return; // 本地开发(根路径):无应用上下文,走 .env.dev
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/" + seg[0] + "/api/env-config?target=" + encodeURIComponent(seg[1]), false);
|
||||||
|
xhr.send(null);
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var conf = JSON.parse(xhr.responseText);
|
||||||
|
if (conf && typeof conf === "object") window.__APP_CONFIG__ = conf;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* 拉取失败保持未设置,走同域推导兜底 */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<title><%- title %></title>
|
<title><%- title %></title>
|
||||||
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
<link rel="stylesheet" href="./libs/ztree/zTreeStyle.css" />
|
||||||
<script src="./libs/jquery.min.js"></script>
|
<script src="./libs/jquery.min.js"></script>
|
||||||
|
|||||||
+3
-3
@@ -6,10 +6,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --mode dev",
|
"dev": "vite --mode dev",
|
||||||
"build": "vite build --mode base",
|
"build": "vite build",
|
||||||
"build:base": "vite build --mode base",
|
|
||||||
"build:ld": "vite build --mode ld",
|
|
||||||
"build:prod": "vite build --mode prod",
|
"build:prod": "vite build --mode prod",
|
||||||
|
"build:ld": "vite build --mode ld",
|
||||||
|
"build:base": "vite build --mode base",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"api:gen": "alova gen -f"
|
"api:gen": "alova gen -f"
|
||||||
},
|
},
|
||||||
|
|||||||
+39
-8
@@ -10,6 +10,7 @@ import Apis from '@/api/index'
|
|||||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||||
import { UserRoleEnum } from "./types";
|
import { UserRoleEnum } from "./types";
|
||||||
|
import { isRouteAllowed } from "@/router";
|
||||||
const store = useDefaultStore();
|
const store = useDefaultStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { $bus } = useCurrentInstance();
|
const { $bus } = useCurrentInstance();
|
||||||
@@ -53,10 +54,9 @@ const layoutInit = (info: any, modList: any[], appList: Array<any>, baseHost: st
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 角色判定
|
* 角色判定(降级兜底):GetUserScope 失败时按 GetUserInfo 的 user.type 推断——
|
||||||
* 现基于 GetUserInfo 返回的 user.type 判定角色:
|
* type=1 → SuperAdmin,其余 → School。同时写入 currentRole 供下游
|
||||||
* type=1 → SuperAdmin,type=2 → School(不再有 Teacher 角色)
|
* Utils.getCurrentRoleInfo 与请求头注入读取(保持兼容;正常路径以 GetUserScope 为准)。
|
||||||
* 同时写入 currentRole 供下游 Utils.getCurrentRoleInfo 读取(保持兼容)
|
|
||||||
*/
|
*/
|
||||||
const setCurrentRole = (user: any) => {
|
const setCurrentRole = (user: any) => {
|
||||||
const userId = user?.id ?? null;
|
const userId = user?.id ?? null;
|
||||||
@@ -87,12 +87,43 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
var user: any = await alovaInstance.Request(Apis.Base.getUserInfo());
|
var user: any = await alovaInstance.Request(Apis.Base.getUserInfo());
|
||||||
|
|
||||||
// 系统字典 & 模块列表(模块当前写死,后续接入接口)
|
|
||||||
await store.fetchSysDict();
|
|
||||||
await store.fetchModules();
|
|
||||||
|
|
||||||
currentUser.value = user;
|
currentUser.value = user;
|
||||||
|
|
||||||
|
// 系统字典
|
||||||
|
await store.fetchSysDict();
|
||||||
|
|
||||||
|
// 角色判定:优先 GetUserScope(后端单点判定四级访问级别,前端不重复实现规则),
|
||||||
|
// 失败时降级走 setCurrentRole 的 type 推断
|
||||||
setCurrentRole(user);
|
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);
|
layoutInit(user, store.modules, store.modules, baseHost, ssoHost);
|
||||||
|
|
||||||
// 登录后直接跳转到左侧菜单第一个可用入口
|
// 登录后直接跳转到左侧菜单第一个可用入口
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export default {
|
|||||||
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
|
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
|
||||||
'PushSubscription.test': ['POST', '/api/push/Test'],
|
'PushSubscription.test': ['POST', '/api/push/Test'],
|
||||||
'PushSubscription.retry': ['POST', '/api/push/Retry/{id}'],
|
'PushSubscription.retry': ['POST', '/api/push/Retry/{id}'],
|
||||||
'Identity.switchIdentity': ['POST', '/api/identity/switch'],
|
|
||||||
'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
|
'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
|
||||||
'GroupAuthz.setGroupModule': ['POST', '/api/groupauthz/SetGroupModule'],
|
'GroupAuthz.setGroupModule': ['POST', '/api/groupauthz/SetGroupModule'],
|
||||||
'GroupAuthz.saveModulePerms': ['POST', '/api/groupauthz/SaveModulePerms'],
|
'GroupAuthz.saveModulePerms': ['POST', '/api/groupauthz/SaveModulePerms'],
|
||||||
@@ -72,6 +71,7 @@ export default {
|
|||||||
'Group.add_2': ['POST', '/oapi/group/add'],
|
'Group.add_2': ['POST', '/oapi/group/add'],
|
||||||
'SysUser.list': ['GET', '/api/sysuser/List'],
|
'SysUser.list': ['GET', '/api/sysuser/List'],
|
||||||
'SysUser.getUserGroups': ['GET', '/api/sysuser/Groups/{userId}'],
|
'SysUser.getUserGroups': ['GET', '/api/sysuser/Groups/{userId}'],
|
||||||
|
'SysUser.userIdentity': ['GET', '/api/sysuser/UserIdentity'],
|
||||||
'SysUser.export_': ['GET', '/api/sysuser/Export'],
|
'SysUser.export_': ['GET', '/api/sysuser/Export'],
|
||||||
'SysUser.downloadTemplate': ['GET', '/api/sysuser/DownloadTemplate'],
|
'SysUser.downloadTemplate': ['GET', '/api/sysuser/DownloadTemplate'],
|
||||||
'SysGroup.sysGroupList': ['GET', '/api/sysgroup/List'],
|
'SysGroup.sysGroupList': ['GET', '/api/sysgroup/List'],
|
||||||
@@ -83,8 +83,6 @@ export default {
|
|||||||
'SysOperationLog.operationLogDownload': ['GET', '/api/oplog/Download/{id}'],
|
'SysOperationLog.operationLogDownload': ['GET', '/api/oplog/Download/{id}'],
|
||||||
'SysOapiLog.oapiLogList': ['GET', '/api/oapilog/List'],
|
'SysOapiLog.oapiLogList': ['GET', '/api/oapilog/List'],
|
||||||
'SysOapiLog.oapiLogDownload': ['GET', '/api/oapilog/Download/{id}'],
|
'SysOapiLog.oapiLogDownload': ['GET', '/api/oapilog/Download/{id}'],
|
||||||
'Identity.current': ['GET', '/api/identity/current'],
|
|
||||||
'Identity.available': ['GET', '/api/identity/available'],
|
|
||||||
'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'],
|
'GroupAuthz.userRoles': ['GET', '/api/groupauthz/UserRoles'],
|
||||||
'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'],
|
'GroupAuthz.userPermissionTree': ['GET', '/api/groupauthz/UserPermissionTree'],
|
||||||
'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'],
|
'GroupAuthz.userGroupTree': ['GET', '/api/groupauthz/UserGroupTree'],
|
||||||
@@ -98,7 +96,9 @@ export default {
|
|||||||
'Base.RedirectTo': ['GET', '/api/base/RedirectTo/{id}'],
|
'Base.RedirectTo': ['GET', '/api/base/RedirectTo/{id}'],
|
||||||
'Base.manualHashCacheDemo': ['GET', '/api/base/ManualHashCacheDemo'],
|
'Base.manualHashCacheDemo': ['GET', '/api/base/ManualHashCacheDemo'],
|
||||||
'Base.getUserInfo': ['GET', '/api/base/GetUserInfo'],
|
'Base.getUserInfo': ['GET', '/api/base/GetUserInfo'],
|
||||||
|
'Base.getUserScope': ['GET', '/api/base/GetUserScope'],
|
||||||
'Base.getSysDict': ['GET', '/api/base/GetSysDict'],
|
'Base.getSysDict': ['GET', '/api/base/GetSysDict'],
|
||||||
|
'AppSettings.schoolList': ['GET', '/api/app/SchoolList'],
|
||||||
'AppRole.roleUsers': ['GET', '/api/approle/RoleUsers'],
|
'AppRole.roleUsers': ['GET', '/api/approle/RoleUsers'],
|
||||||
'AppRole.roleModuleTree': ['GET', '/api/approle/RoleModuleTree'],
|
'AppRole.roleModuleTree': ['GET', '/api/approle/RoleModuleTree'],
|
||||||
'AppRole.roleList': ['GET', '/api/approle/RoleList'],
|
'AppRole.roleList': ['GET', '/api/approle/RoleList'],
|
||||||
|
|||||||
Vendored
+79
-104
@@ -114,10 +114,17 @@ export interface SysUserRequest {
|
|||||||
email?: string;
|
email?: string;
|
||||||
gender?: number;
|
gender?: number;
|
||||||
profilePicture?: string;
|
profilePicture?: string;
|
||||||
roles?: number[];
|
/** 身份归属学校(schoolId 解析后的真实组 id;编辑必传,新增可不传按 groupId 解析) */
|
||||||
|
schoolId?: number;
|
||||||
|
/** 用户身份角色(SysRoleEnum.key 1-7),按 schoolId 全量覆盖保存;空数组=清空该校身份 */
|
||||||
|
roleIds?: number[];
|
||||||
disabled?: number;
|
disabled?: number;
|
||||||
expired?: boolean;
|
expired?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface UserIdentityVo {
|
||||||
|
schoolId?: number;
|
||||||
|
roleIds?: number[];
|
||||||
|
}
|
||||||
export interface SysGroupSortRequest {
|
export interface SysGroupSortRequest {
|
||||||
pid: number;
|
pid: number;
|
||||||
ids: number[];
|
ids: number[];
|
||||||
@@ -146,10 +153,6 @@ export interface PushTestRequest {
|
|||||||
interfacePath?: string;
|
interfacePath?: string;
|
||||||
targetIds?: number[];
|
targetIds?: number[];
|
||||||
}
|
}
|
||||||
export interface IdentitySwitchRequest {
|
|
||||||
roleId: number;
|
|
||||||
schoolId: number;
|
|
||||||
}
|
|
||||||
export interface SchoolAppAuthzRequest {
|
export interface SchoolAppAuthzRequest {
|
||||||
schid: number;
|
schid: number;
|
||||||
appid: number;
|
appid: number;
|
||||||
@@ -233,20 +236,6 @@ export interface SysGroupVo {
|
|||||||
sort?: number;
|
sort?: number;
|
||||||
children?: SysGroupVo[];
|
children?: SysGroupVo[];
|
||||||
}
|
}
|
||||||
export interface IdentityCurrentVo {
|
|
||||||
roleId?: number;
|
|
||||||
roleName?: string;
|
|
||||||
schoolId?: number;
|
|
||||||
schoolName?: string;
|
|
||||||
fallback?: boolean;
|
|
||||||
}
|
|
||||||
export interface IdentityOptionVo {
|
|
||||||
roleId?: number;
|
|
||||||
roleName?: string;
|
|
||||||
schoolId?: number;
|
|
||||||
schoolName?: string;
|
|
||||||
isMain?: boolean;
|
|
||||||
}
|
|
||||||
export interface AppItem {
|
export interface AppItem {
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -319,6 +308,9 @@ export interface SysUserVo {
|
|||||||
disabled?: number;
|
disabled?: number;
|
||||||
type?: number;
|
type?: number;
|
||||||
sortNo?: number;
|
sortNo?: number;
|
||||||
|
groupStatus?: number;
|
||||||
|
/** 主校身份名(如「老师/校领导」):列表「用户身份」列展示字段(roles 列已停写) */
|
||||||
|
identityNames?: string;
|
||||||
expired?: boolean;
|
expired?: boolean;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
@@ -1281,6 +1273,40 @@ declare global {
|
|||||||
>(
|
>(
|
||||||
config: Config
|
config: Config
|
||||||
): Alova2Method<number[], 'SysUser.getUserGroups', Config>;
|
): Alova2Method<number[], 'SysUser.getUserGroups', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET]
|
||||||
|
*
|
||||||
|
* **path:** /api/sysuser/UserIdentity
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Query Parameters**
|
||||||
|
* ```ts
|
||||||
|
* type QueryParameters = {
|
||||||
|
* userId: number
|
||||||
|
* groupId: number
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = UserIdentityVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
userIdentity<
|
||||||
|
Config extends Alova2MethodConfig<UserIdentityVo> & {
|
||||||
|
params: {
|
||||||
|
userId: number;
|
||||||
|
groupId: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<UserIdentityVo, 'SysUser.userIdentity', Config>;
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
@@ -1835,91 +1861,6 @@ declare global {
|
|||||||
config: Config
|
config: Config
|
||||||
): Alova2Method<Blob, 'PushSubscription.download', Config>;
|
): Alova2Method<Blob, 'PushSubscription.download', Config>;
|
||||||
};
|
};
|
||||||
Identity: {
|
|
||||||
/**
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* [POST]
|
|
||||||
*
|
|
||||||
* **path:** /api/identity/switch
|
|
||||||
*
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* **RequestBody**
|
|
||||||
* ```ts
|
|
||||||
* type RequestBody = {
|
|
||||||
* roleId: number
|
|
||||||
* schoolId: number
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* **Response**
|
|
||||||
* ```ts
|
|
||||||
* type Response = {
|
|
||||||
* roleId?: number
|
|
||||||
* roleName?: string
|
|
||||||
* schoolId?: number
|
|
||||||
* schoolName?: string
|
|
||||||
* fallback?: boolean
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
switchIdentity<
|
|
||||||
Config extends Alova2MethodConfig<IdentityCurrentVo> & {
|
|
||||||
data: IdentitySwitchRequest;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
config: Config
|
|
||||||
): Alova2Method<IdentityCurrentVo, 'Identity.switchIdentity', Config>;
|
|
||||||
/**
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* [GET]
|
|
||||||
*
|
|
||||||
* **path:** /api/identity/current
|
|
||||||
*
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* **Response**
|
|
||||||
* ```ts
|
|
||||||
* type Response = {
|
|
||||||
* roleId?: number
|
|
||||||
* roleName?: string
|
|
||||||
* schoolId?: number
|
|
||||||
* schoolName?: string
|
|
||||||
* fallback?: boolean
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
current<Config extends Alova2MethodConfig<IdentityCurrentVo>>(
|
|
||||||
config?: Config
|
|
||||||
): Alova2Method<IdentityCurrentVo, 'Identity.current', Config>;
|
|
||||||
/**
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* [GET]
|
|
||||||
*
|
|
||||||
* **path:** /api/identity/available
|
|
||||||
*
|
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* **Response**
|
|
||||||
* ```ts
|
|
||||||
* type Response = Array<{
|
|
||||||
* roleId?: number
|
|
||||||
* roleName?: string
|
|
||||||
* schoolId?: number
|
|
||||||
* schoolName?: string
|
|
||||||
* isMain?: boolean
|
|
||||||
* }>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
available<Config extends Alova2MethodConfig<IdentityOptionVo[]>>(
|
|
||||||
config?: Config
|
|
||||||
): Alova2Method<IdentityOptionVo[], 'Identity.available', Config>;
|
|
||||||
};
|
|
||||||
GroupAuthz: {
|
GroupAuthz: {
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
@@ -2817,6 +2758,23 @@ declare global {
|
|||||||
getUserInfo<Config extends Alova2MethodConfig<object>>(
|
getUserInfo<Config extends Alova2MethodConfig<object>>(
|
||||||
config?: Config
|
config?: Config
|
||||||
): Alova2Method<object, 'Base.getUserInfo', 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>;
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
@@ -3979,6 +3937,23 @@ declare global {
|
|||||||
>(
|
>(
|
||||||
config: Config
|
config: Config
|
||||||
): Alova2Method<BasePageResultApplicationInfoVo, 'AppSettings.appList', 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>;
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
import { BaseConf } from "@/types/Basics";
|
import { BaseConf } from "@/types/Basics";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 Vite 环境变量管理配置,不再依赖 window.conf / config.js
|
* 配置优先级(高 → 低):
|
||||||
* 环境变量定义在项目根目录的 .env.dev / .env.base / .env.ld 中
|
* ① window.__APP_CONFIG__:线上运行时配置,index.html 引导脚本在应用模块加载前同步拉取
|
||||||
|
* /<应用>/<前端目标>/api/env-config?target=xxx(projman 配置管理在线维护、双写 Nacos)写入
|
||||||
|
* ② import.meta.env.VITE_*:仅本地开发(vite dev --mode dev 读 .env.dev)——值可自由指本地
|
||||||
|
* 或线上地址,供联调切换;build(production mode)时 .env/.env.production 均不存在,
|
||||||
|
* VITE_* 恒为 undefined,本层自动跳过,产物不含任何环境值
|
||||||
|
* ③ 同域推导兜底:env-config 拉取失败(线上后端/Nacos 不可用)时框架至少能渲染;
|
||||||
|
* ssoHost/relayConf 为空 → 登录与跨域中转不可用——与后端不可用状态一致,可接受。
|
||||||
*/
|
*/
|
||||||
const env = import.meta.env;
|
const env = import.meta.env;
|
||||||
|
|
||||||
|
// ③ 同域推导兜底:context 段取当前路径首段(/userbase/front/ → userbase),与服务端 context-path 对齐
|
||||||
|
const ctxSeg = (window.location.pathname.split("/").filter(Boolean)[0] || "") as string;
|
||||||
|
const origin = window.location.origin;
|
||||||
|
|
||||||
let conf: BaseConf = {
|
let conf: BaseConf = {
|
||||||
|
baseHost: origin,
|
||||||
|
ssoHost: "",
|
||||||
|
serviceHost: ctxSeg ? `${origin}/${ctxSeg}` : origin,
|
||||||
|
relayConf: {
|
||||||
|
ziyuankuHost: "",
|
||||||
|
zhuanxiangHost: "",
|
||||||
|
},
|
||||||
|
isMock: false,
|
||||||
|
isDebug: env.DEV,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 逐键覆盖:只覆盖来源里实际出现且非空的键(含 relayConf 二级逐键合并),未出现的键沿用底层值
|
||||||
|
const applyConf = (target: any, source: any) => {
|
||||||
|
if (!source || typeof source !== "object") return;
|
||||||
|
const { relayConf, ...flat } = source;
|
||||||
|
Object.keys(flat).forEach(key => {
|
||||||
|
if (flat[key] !== undefined && flat[key] !== null && flat[key] !== "") {
|
||||||
|
(target as any)[key] = flat[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (relayConf && typeof relayConf === "object") {
|
||||||
|
if (!target.relayConf || typeof target.relayConf !== "object") target.relayConf = {};
|
||||||
|
Object.keys(relayConf).forEach(key => {
|
||||||
|
if (relayConf[key]) target.relayConf[key] = relayConf[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ② 本地开发(.env.dev;构建产物中恒 undefined → 自动跳过)
|
||||||
|
applyConf(conf, {
|
||||||
baseHost: env.VITE_BASE_HOST,
|
baseHost: env.VITE_BASE_HOST,
|
||||||
ssoHost: env.VITE_SSO_HOST,
|
ssoHost: env.VITE_SSO_HOST,
|
||||||
serviceHost: env.VITE_SERVICE_HOST,
|
serviceHost: env.VITE_SERVICE_HOST,
|
||||||
@@ -15,12 +55,13 @@ let conf: BaseConf = {
|
|||||||
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
zhuanxiangHost: env.VITE_ZHUANXIANG_HOST,
|
||||||
},
|
},
|
||||||
isMock: env.VITE_IS_MOCK === "true",
|
isMock: env.VITE_IS_MOCK === "true",
|
||||||
isDebug: env.DEV,
|
});
|
||||||
};
|
|
||||||
|
// ① 线上运行时配置(最高优先,覆盖前面所有)
|
||||||
|
applyConf(conf, (window as any).__APP_CONFIG__);
|
||||||
|
|
||||||
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
// HTTPS 自动升级:当前页面是 https 时,将所有 http:// 配置项升级为 https://
|
||||||
var ishttps = "https:" == document.location.protocol ? true : false;
|
if ("https:" === document.location.protocol) {
|
||||||
if (ishttps) {
|
|
||||||
Object.keys(conf).forEach(element => {
|
Object.keys(conf).forEach(element => {
|
||||||
if (typeof conf[element] === "string" && conf[element].indexOf("http://") > -1) {
|
if (typeof conf[element] === "string" && conf[element].indexOf("http://") > -1) {
|
||||||
conf[element] = conf[element].replace("http://", "https://");
|
conf[element] = conf[element].replace("http://", "https://");
|
||||||
|
|||||||
+46
-22
@@ -1,26 +1,23 @@
|
|||||||
import type { IModule } from "@/store";
|
import type { IModule } from "@/store";
|
||||||
|
import { UserRoleEnum } from "@/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 左侧菜单数据(系统管理功能分组)
|
* 左侧菜单数据(系统管理功能分组)+ 菜单权限矩阵(管理端权限分级改造 §4.2)。
|
||||||
* ----------------------------------------------------------------
|
* ----------------------------------------------------------------
|
||||||
* 当前直接 Promise.resolve 返回写死数据;后续接入真实接口时,
|
* 每个节点带 roles 元数据(可访问级别集合),getMenuList(userRole) 按级别过滤,
|
||||||
* 只需把 `return Promise.resolve(menu)` 替换为
|
* children 过滤后为空的父目录整体隐藏。当前数据写死;后续接入真实接口时,
|
||||||
* `return alovaInstance.Request(Apis.xxx.getMenu())`
|
* 只需把 `return Promise.resolve(...)` 替换为接口调用后 filterMenuByRole 即可。
|
||||||
* 即可无缝对接,调用方(store.fetchModules)无需任何改动。
|
|
||||||
*
|
*
|
||||||
* 分组结构:
|
* 分组结构:
|
||||||
* 1. 组,用户管理 —— 顶层独立选项卡(单页)
|
* 1. 组,用户管理 —— S+B+C
|
||||||
* 2. 应用管理 —— 顶层独立选项卡(= 应用列表主页 /app-manage/list);
|
* 2. 应用管理 —— S+C(局端不开放:局端的应用动作是「开通」,应用维护是平台级)
|
||||||
* 页内按钮跳转「应用类别管理」子页(/app-manage/category,不在菜单里,
|
* 3. 权限管理 —— 父目录:应用开通(S+B) / 组授权 / 角色授权 / 权限一览(S+B+C)
|
||||||
* 由 routes.ts 的 meta.parentId=1102 关联,面包屑「应用管理>应用类别管理」)
|
* 4. 日志管理 —— 仅超管(父目录)
|
||||||
* 3. 权限管理 —— 父目录:应用开通 / 组授权 / 权限一览
|
|
||||||
* 4. 日志管理 —— 父目录:接口日志 / 系统日志
|
|
||||||
*
|
*
|
||||||
* 每个节点带唯一 id;子节点的 pid 由 layout.vue 的 flaten() 扁平化时
|
* 每个节点带唯一 id;子节点的 pid 由 layout.vue 的 flaten() 扁平化时
|
||||||
* 依据父节点 id 自动推导,用于「高亮自身 + 面包屑展示父级」联动。
|
* 依据父节点 id 自动推导,用于「高亮自身 + 面包屑展示父级」联动。
|
||||||
* 菜单项不再渲染 icon。
|
|
||||||
*/
|
*/
|
||||||
export function getMenuList(): Promise<IModule[]> {
|
export function getMenuList(userRole?: UserRoleEnum): Promise<IModule[]> {
|
||||||
const menu: IModule[] = [
|
const menu: IModule[] = [
|
||||||
// 1. 组,用户管理(顶层独立选项卡)
|
// 1. 组,用户管理(顶层独立选项卡)
|
||||||
{
|
{
|
||||||
@@ -28,6 +25,7 @@ export function getMenuList(): Promise<IModule[]> {
|
|||||||
modId: 1101,
|
modId: 1101,
|
||||||
name: "组,用户管理",
|
name: "组,用户管理",
|
||||||
page: "/group-user",
|
page: "/group-user",
|
||||||
|
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School],
|
||||||
},
|
},
|
||||||
// 2. 应用管理(顶层独立选项卡,= 应用列表主页;页内按钮跳转应用类别管理子页)
|
// 2. 应用管理(顶层独立选项卡,= 应用列表主页;页内按钮跳转应用类别管理子页)
|
||||||
{
|
{
|
||||||
@@ -35,6 +33,7 @@ export function getMenuList(): Promise<IModule[]> {
|
|||||||
modId: 1102,
|
modId: 1102,
|
||||||
name: "应用管理",
|
name: "应用管理",
|
||||||
page: "/app-manage/list",
|
page: "/app-manage/list",
|
||||||
|
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.School],
|
||||||
},
|
},
|
||||||
// 3. 权限管理(父目录)
|
// 3. 权限管理(父目录)
|
||||||
{
|
{
|
||||||
@@ -43,11 +42,12 @@ export function getMenuList(): Promise<IModule[]> {
|
|||||||
name: "权限管理",
|
name: "权限管理",
|
||||||
page: "",
|
page: "",
|
||||||
childState: true,
|
childState: true,
|
||||||
|
roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School],
|
||||||
children: [
|
children: [
|
||||||
{ id: 41, modId: 1103, name: "应用开通", page: "/app-open" },
|
{ id: 41, modId: 1103, name: "应用开通", page: "/app-open", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau] },
|
||||||
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth" },
|
{ id: 42, modId: 1104, name: "组授权", page: "/group-auth", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||||
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list" },
|
{ id: 44, modId: 1110, name: "角色授权", page: "/role-authz/list", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||||
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view" },
|
{ id: 43, modId: 1105, name: "权限一览", page: "/permission-view", roles: [UserRoleEnum.SuperAdmin, UserRoleEnum.Bureau, UserRoleEnum.School] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 4. 日志管理(父目录)
|
// 4. 日志管理(父目录)
|
||||||
@@ -57,13 +57,37 @@ export function getMenuList(): Promise<IModule[]> {
|
|||||||
name: "日志管理",
|
name: "日志管理",
|
||||||
page: "",
|
page: "",
|
||||||
childState: true,
|
childState: true,
|
||||||
|
roles: [UserRoleEnum.SuperAdmin],
|
||||||
children: [
|
children: [
|
||||||
{ id: 53, modId: 1111, name: "操作日志", page: "/oplog" },
|
{ id: 53, modId: 1111, name: "操作日志", page: "/oplog", roles: [UserRoleEnum.SuperAdmin] },
|
||||||
{ id: 56, modId: 1114, name: "开放接口日志", page: "/oapilog" },
|
{ id: 56, modId: 1114, name: "开放接口日志", page: "/oapilog", roles: [UserRoleEnum.SuperAdmin] },
|
||||||
{ id: 54, modId: 1112, name: "推送日志", page: "/push-log" },
|
{ id: 54, modId: 1112, name: "推送日志", page: "/push-log", roles: [UserRoleEnum.SuperAdmin] },
|
||||||
{ id: 55, modId: 1113, name: "测试推送", page: "/push-test" },
|
{ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -69,7 +69,8 @@ export default {
|
|||||||
// console.log(event, treeId, treeNode, clickFlag);
|
// console.log(event, treeId, treeNode, clickFlag);
|
||||||
if (_this.isCheck) {
|
if (_this.isCheck) {
|
||||||
var treeObj = $.fn.zTree.getZTreeObj(treeId);
|
var treeObj = $.fn.zTree.getZTreeObj(treeId);
|
||||||
treeObj.checkNode(treeNode, !treeNode.checked, true);
|
// 第4参 callbackFlag=true:点击文字切换勾选时同步触发 onCheck,与点击勾选框行为一致
|
||||||
|
treeObj.checkNode(treeNode, !treeNode.checked, true, true);
|
||||||
}
|
}
|
||||||
_this.$emit("on-tree-click", treeNode);
|
_this.$emit("on-tree-click", treeNode);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const linkUserInfo = () => {
|
|||||||
};
|
};
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
let url = window.location.href;
|
let url = window.location.href;
|
||||||
url = "/logout?service=" + url;
|
url = "/cas/logout?service=" + url;
|
||||||
window.location.href = ssoHost + url;
|
window.location.href = ssoHost + url;
|
||||||
};
|
};
|
||||||
const childChange = (index: number) => {
|
const childChange = (index: number) => {
|
||||||
|
|||||||
+36
-1
@@ -1,9 +1,44 @@
|
|||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
|
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from "vue-router";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
|
import { useDefaultStore } from "@/store";
|
||||||
|
import { UserRoleEnum } from "@/types";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前角色是否可访问目标路由(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;
|
export default router;
|
||||||
+28
-14
@@ -1,4 +1,5 @@
|
|||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
import { UserRoleEnum } from "@/types";
|
||||||
import GroupUser from "@/views/group-user/index.vue";
|
import GroupUser from "@/views/group-user/index.vue";
|
||||||
import AppList from "@/views/app/AppList.vue";
|
import AppList from "@/views/app/AppList.vue";
|
||||||
import AppCategory from "@/views/app/AppCategory.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 OapiLogList from "@/views/oapilog/OapiLogList.vue";
|
||||||
import PushLog from "@/views/push/PushLog.vue";
|
import PushLog from "@/views/push/PushLog.vue";
|
||||||
import PushTest from "@/views/push/PushTest.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[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
redirect: "/group-user",
|
redirect: "/group-user",
|
||||||
},
|
},
|
||||||
|
// ===== 无权限页(路由守卫重定向目标)=====
|
||||||
|
{
|
||||||
|
path: "/no-auth",
|
||||||
|
name: "NoAuth",
|
||||||
|
component: NoAuth,
|
||||||
|
meta: { title: "暂无权限" },
|
||||||
|
},
|
||||||
// ===== 开发组(组/用户管理,多页嵌套目录)=====
|
// ===== 开发组(组/用户管理,多页嵌套目录)=====
|
||||||
{
|
{
|
||||||
path: "/group-user",
|
path: "/group-user",
|
||||||
name: "GroupUser",
|
name: "GroupUser",
|
||||||
component: GroupUser,
|
component: GroupUser,
|
||||||
meta: { title: "组,用户管理", icon: "icon-list", modId: 1101 },
|
meta: { title: "组,用户管理", icon: "icon-list", modId: 1101, roles: ALL_ADMIN },
|
||||||
},
|
},
|
||||||
// ===== 应用管理(单选项卡)=====
|
// ===== 应用管理(单选项卡)=====
|
||||||
// 应用管理主页(= 应用列表),菜单顶层「应用管理」指向此路由(modId 1102)
|
// 应用管理主页(= 应用列表),菜单顶层「应用管理」指向此路由(modId 1102)
|
||||||
|
// 局端不开放应用管理(局端的应用动作是「开通」)
|
||||||
{
|
{
|
||||||
path: "/app-manage/list",
|
path: "/app-manage/list",
|
||||||
name: "AppList",
|
name: "AppList",
|
||||||
component: 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 关联父菜单「应用管理」,
|
// 应用类型管理(深层子页,不在菜单里):meta.parentId=1102 关联父菜单「应用管理」,
|
||||||
// 由 layout.vue 的 routeChange 推导面包屑「应用管理>应用类型管理」并高亮父菜单
|
// 由 layout.vue 的 routeChange 推导面包屑「应用管理>应用类型管理」并高亮父菜单
|
||||||
|
// 校管可:类别改名/排序(写覆盖表);新增/删除类别按钮由页面按级别隐藏
|
||||||
{
|
{
|
||||||
path: "/app-manage/category",
|
path: "/app-manage/category",
|
||||||
name: "AppCategory",
|
name: "AppCategory",
|
||||||
component: 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",
|
path: "/app-manage/module",
|
||||||
name: "ModuleManage",
|
name: "ModuleManage",
|
||||||
component: ModuleManage,
|
component: ModuleManage,
|
||||||
meta: { title: "模块管理", modId: 11023, parentId: 1102 },
|
meta: { title: "模块管理", modId: 11023, parentId: 1102, roles: [UserRoleEnum.SuperAdmin] },
|
||||||
},
|
},
|
||||||
// /app-manage 兜底重定向到主页
|
// /app-manage 兜底重定向到主页
|
||||||
{
|
{
|
||||||
@@ -59,7 +73,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: "/role-authz/list",
|
path: "/role-authz/list",
|
||||||
name: "RoleAuthzList",
|
name: "RoleAuthzList",
|
||||||
component: 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 关联父菜单「角色授权」,
|
// 角色管理(深层子页,不在菜单里):meta.parentId=1110 关联父菜单「角色授权」,
|
||||||
// 由 layout.vue 推导面包屑「角色授权>角色管理」并高亮父菜单(与 /app-manage/category 范式一致)
|
// 由 layout.vue 推导面包屑「角色授权>角色管理」并高亮父菜单(与 /app-manage/category 范式一致)
|
||||||
@@ -67,7 +81,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: "/role-authz/manage",
|
path: "/role-authz/manage",
|
||||||
name: "RoleManage",
|
name: "RoleManage",
|
||||||
component: RoleManage,
|
component: RoleManage,
|
||||||
meta: { title: "角色管理", modId: 11101, parentId: 1110 },
|
meta: { title: "角色管理", modId: 11101, parentId: 1110, roles: ALL_ADMIN },
|
||||||
},
|
},
|
||||||
// /role-authz 兜底重定向到主页
|
// /role-authz 兜底重定向到主页
|
||||||
{
|
{
|
||||||
@@ -78,45 +92,45 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: "/app-open",
|
path: "/app-open",
|
||||||
name: "AppOpen",
|
name: "AppOpen",
|
||||||
component: 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",
|
path: "/group-auth",
|
||||||
name: "GroupAuth",
|
name: "GroupAuth",
|
||||||
component: GroupAuth,
|
component: GroupAuth,
|
||||||
meta: { title: "组授权", icon: "icon-list", modId: 1104 },
|
meta: { title: "组授权", icon: "icon-list", modId: 1104, roles: ALL_ADMIN },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/permission-view",
|
path: "/permission-view",
|
||||||
name: "PermissionView",
|
name: "PermissionView",
|
||||||
component: PermissionView,
|
component: PermissionView,
|
||||||
meta: { title: "权限一览", icon: "icon-list", modId: 1105 },
|
meta: { title: "权限一览", icon: "icon-list", modId: 1105, roles: ALL_ADMIN },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/push-log",
|
path: "/push-log",
|
||||||
name: "PushLog",
|
name: "PushLog",
|
||||||
component: 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",
|
path: "/push-test",
|
||||||
name: "PushTest",
|
name: "PushTest",
|
||||||
component: PushTest,
|
component: PushTest,
|
||||||
meta: { title: "测试推送", icon: "icon-list", modId: 1113 },
|
meta: { title: "测试推送", icon: "icon-list", modId: 1113, roles: [UserRoleEnum.SuperAdmin] },
|
||||||
},
|
},
|
||||||
// ===== 操作日志 =====
|
// ===== 操作日志 =====
|
||||||
{
|
{
|
||||||
path: "/oplog",
|
path: "/oplog",
|
||||||
name: "OperationLogList",
|
name: "OperationLogList",
|
||||||
component: 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",
|
path: "/oapilog",
|
||||||
name: "OapiLogList",
|
name: "OapiLogList",
|
||||||
component: 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 {
|
export interface IdefaultStore {
|
||||||
curStatsDate: Date;
|
curStatsDate: Date;
|
||||||
userRole: UserRoleEnum;
|
userRole: UserRoleEnum;
|
||||||
|
/** 管理端访问范围(GetUserScope 返回,后端单点判定):BUREAU 区域组 / SCHOOL 可管理学校组 */
|
||||||
|
userScope: { bureauGroupId?: number | null; schoolGroupIds?: number[] | null };
|
||||||
lenLevel: Array<SchLenLevel>;
|
lenLevel: Array<SchLenLevel>;
|
||||||
subjects: Array<Subject>;
|
subjects: Array<Subject>;
|
||||||
/** 系统字典(GetSysDict 接口返回) */
|
/** 系统字典(GetSysDict 接口返回) */
|
||||||
sysDict: Record<string, any>;
|
sysDict: Record<string, any>;
|
||||||
/** 模块列表(当前写死,后续接入接口) */
|
/** 模块列表(当前写死,后续接入接口) */
|
||||||
modules: Array<IModule>;
|
modules: Array<IModule>;
|
||||||
|
/** 角色是否已落定(App.vue GetUserScope/降级判定完成后置 true,路由守卫依赖) */
|
||||||
|
roleLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDefaultStore = defineStore("defaultStore", {
|
export const useDefaultStore = defineStore("defaultStore", {
|
||||||
@@ -35,10 +39,12 @@ export const useDefaultStore = defineStore("defaultStore", {
|
|||||||
return {
|
return {
|
||||||
curStatsDate: new Date(),
|
curStatsDate: new Date(),
|
||||||
userRole: UserRoleEnum.School,
|
userRole: UserRoleEnum.School,
|
||||||
|
userScope: { bureauGroupId: null, schoolGroupIds: null },
|
||||||
lenLevel: [],
|
lenLevel: [],
|
||||||
subjects: [],
|
subjects: [],
|
||||||
sysDict: {},
|
sysDict: {},
|
||||||
modules: [],
|
modules: [],
|
||||||
|
roleLoaded: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@@ -48,6 +54,12 @@ export const useDefaultStore = defineStore("defaultStore", {
|
|||||||
setUserRole(role: UserRoleEnum) {
|
setUserRole(role: UserRoleEnum) {
|
||||||
this.userRole = role;
|
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>) {
|
setSchLenLevel(len: Array<SchLenLevel>) {
|
||||||
this.lenLevel = len;
|
this.lenLevel = len;
|
||||||
},
|
},
|
||||||
@@ -61,9 +73,9 @@ export const useDefaultStore = defineStore("defaultStore", {
|
|||||||
this.sysDict = {};
|
this.sysDict = {};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/** 获取模块列表(数据来源见 src/assets/config/menu.ts,后续接入接口无缝替换) */
|
/** 获取模块列表(数据源 src/assets/config/menu.ts,按当前级别过滤菜单权限矩阵) */
|
||||||
async fetchModules() {
|
async fetchModules() {
|
||||||
this.modules = await getMenuList();
|
this.modules = await getMenuList(this.userRole);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ export enum OperatorEnum {
|
|||||||
sel,
|
sel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端访问级别(与后端 AccessLevelEnum / GetUserScope.level 对齐,后端单点判定)。
|
||||||
|
* 注意:School 由旧值 2 改为 3(原枚举值无消费方依赖具体数值,安全重排)。
|
||||||
|
*/
|
||||||
export enum UserRoleEnum {
|
export enum UserRoleEnum {
|
||||||
SuperAdmin = 1,
|
SuperAdmin = 1,
|
||||||
|
Bureau,
|
||||||
School,
|
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>
|
||||||
@@ -19,8 +19,9 @@
|
|||||||
<template v-else>未找到匹配用户</template>
|
<template v-else>未找到匹配用户</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="user-list">
|
<div v-else class="user-list">
|
||||||
<div v-for="user in userList" :key="user.id" class="user-item"
|
<div v-for="user in userList" :key="`${user.id}-${user.groupId ?? ''}`" class="user-item"
|
||||||
:class="{ active: selectedUser?.id === user.id }" @click="onSelectUser(user)">
|
:class="{ active: selectedUser?.id === user.id && selectedUser?.groupId === user.groupId }"
|
||||||
|
@click="onSelectUser(user)">
|
||||||
<div class="user-name">{{ userDisplayName(user) }}</div>
|
<div class="user-name">{{ userDisplayName(user) }}</div>
|
||||||
<div v-if="mode === 'user' && user.groupPath" class="user-path">{{ user.groupPath }}</div>
|
<div v-if="mode === 'user' && user.groupPath" class="user-path">{{ user.groupPath }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +85,8 @@ interface UserItem {
|
|||||||
account?: string;
|
account?: string;
|
||||||
realName?: string;
|
realName?: string;
|
||||||
isMain?: number;
|
isMain?: number;
|
||||||
|
/** 该行对应的组ID:同一用户多组时后端每个组出一行,选中行按该组查看权限 */
|
||||||
|
groupId?: number;
|
||||||
groupPath?: string;
|
groupPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +235,8 @@ async function onSelectUser(user: UserItem) {
|
|||||||
selectedUser.value = user;
|
selectedUser.value = user;
|
||||||
treeLoading.value = true;
|
treeLoading.value = true;
|
||||||
const userId = user.id as number;
|
const userId = user.id as number;
|
||||||
const groupId = selectedGroup.value?.id;
|
// 行自带组ID(多组用户每个组一行)优先;组列表模式回退左侧选中组
|
||||||
|
const groupId = user.groupId ?? (selectedGroup.value?.id ? Number(selectedGroup.value.id) : undefined);
|
||||||
// 三块数据并行加载,互不阻塞
|
// 三块数据并行加载,互不阻塞
|
||||||
// userGroupTree 为新增接口:apiDefinitions 未重新生成(npm run api:gen)时方法不存在,降级为友好报错
|
// userGroupTree 为新增接口:apiDefinitions 未重新生成(npm run api:gen)时方法不存在,降级为友好报错
|
||||||
const userGroupTreeApi = (Apis.GroupAuthz as any).userGroupTree
|
const userGroupTreeApi = (Apis.GroupAuthz as any).userGroupTree
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<Alert show-icon>设置说明:拖动排序,被使用的分类不能删除</Alert>
|
<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"
|
<draggable class="type_set-wrap fix" tag="ul" v-model="typeList" :group="'appType'" @change="typeSortSave"
|
||||||
item-key="id">
|
item-key="id">
|
||||||
<template #item="{ element }">
|
<template #item="{ element }">
|
||||||
@@ -16,7 +22,7 @@
|
|||||||
<span>编辑</span>
|
<span>编辑</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action_delete" @click="removeSave(element)">
|
<div class="action_delete" v-if="!isSchoolMode" @click="removeSave(element)">
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,16 +55,25 @@
|
|||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
|
||||||
|
|
||||||
const { data: typeList, send: sendTypeList } = useRequest(
|
// 学校管理员上下文:仅改名/排序(写学校覆盖表);新增/删除类型仅超管(按钮隐藏 + 接口 requireSuper 双挡)
|
||||||
Apis.AppSettings.appTypeList({
|
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
|
||||||
transform: (res: any) => {
|
|
||||||
return res ?? [];
|
const typeList = ref<any[]>([])
|
||||||
},
|
|
||||||
cacheFor: null,
|
async function fetchTypeList() {
|
||||||
}),
|
try {
|
||||||
{ initialData: [], immediate: true }
|
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 refFormData = useTemplateRef('formData');
|
||||||
const editModal = ref(false);
|
const editModal = ref(false);
|
||||||
@@ -80,13 +95,22 @@ async function addModuleShow() {
|
|||||||
editModal.value = true;
|
editModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//拖动排序:提交当前顺序的 id 数组
|
//拖动排序:提交当前顺序的 id 数组(SCHOOL 模式写学校覆盖表快照;多校须先选定学校)
|
||||||
async function typeSortSave() {
|
async function typeSortSave() {
|
||||||
|
if (!requireSchool()) {
|
||||||
|
// 未选学校:拒绝提交并回拉列表,撤销拖拽造成的本地顺序变化
|
||||||
|
await fetchTypeList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const ids = (typeList.value ?? []).map((s: any) => s.id);
|
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) {
|
async function showSetting(element: any) {
|
||||||
|
if (!requireSchool()) return;
|
||||||
currentId.value = element.id;
|
currentId.value = element.id;
|
||||||
form.value = {
|
form.value = {
|
||||||
id: element.id,
|
id: element.id,
|
||||||
@@ -106,7 +130,7 @@ async function removeSave(element: any) {
|
|||||||
try {
|
try {
|
||||||
await Apis.AppSettings.deleteType({ pathParams: { id: element.id } });
|
await Apis.AppSettings.deleteType({ pathParams: { id: element.id } });
|
||||||
Message.success("操作成功!");
|
Message.success("操作成功!");
|
||||||
await sendTypeList();
|
await fetchTypeList();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -138,11 +162,13 @@ async function onRawEditSave() {
|
|||||||
id: currentId.value,
|
id: currentId.value,
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
},
|
},
|
||||||
|
// SCHOOL 模式:改名写学校覆盖表(仅该校生效)
|
||||||
|
params: currentSchoolId.value != null ? { schoolId: currentSchoolId.value } : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Message.success("操作成功!");
|
Message.success("操作成功!");
|
||||||
editModal.value = false;
|
editModal.value = false;
|
||||||
await sendTypeList();
|
await fetchTypeList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -163,6 +189,10 @@ function onReset() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.school-select {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.type_set-wrap {
|
.type_set-wrap {
|
||||||
margin: 12px 0 16px;
|
margin: 12px 0 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-16
@@ -2,14 +2,14 @@
|
|||||||
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
|
<Modal v-model="show" :mask-closable="false" width="800" scrollable :title="title"
|
||||||
@on-visible-change="onVisibleChange">
|
@on-visible-change="onVisibleChange">
|
||||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="app-form width-95">
|
<Form ref="formRef" :model="form" :rules="rules" :label-width="130" class="app-form width-95">
|
||||||
<!-- 第一行:应用名称 / 应用类型 -->
|
<!-- 第一行:应用名称 / 应用类型(学校模式隐藏类型——类型归属平台主数据) -->
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="12">
|
<Col span="12">
|
||||||
<FormItem label="应用名称:" prop="name">
|
<FormItem label="应用名称:" prop="name">
|
||||||
<Input v-model="form.name" placeholder="请输入应用名称" />
|
<Input v-model="form.name" placeholder="请输入应用名称" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span="12">
|
<Col span="12" v-if="!schoolMode">
|
||||||
<FormItem label="应用类型:" prop="categoryId">
|
<FormItem label="应用类型:" prop="categoryId">
|
||||||
<Select v-model="form.categoryId" transfer placeholder="请选择应用类型">
|
<Select v-model="form.categoryId" transfer placeholder="请选择应用类型">
|
||||||
<Option v-for="item in categoryList" :key="item.id" :value="item.id" :label="item.name" />
|
<Option v-for="item in categoryList" :key="item.id" :value="item.id" :label="item.name" />
|
||||||
@@ -26,8 +26,15 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</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">
|
<Col span="12">
|
||||||
<FormItem label="appKey:">
|
<FormItem label="appKey:">
|
||||||
<Input :model-value="form.appKey?.toString()" readonly />
|
<Input :model-value="form.appKey?.toString()" readonly />
|
||||||
@@ -43,8 +50,8 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第四行:应用前端类型(tag 多选) -->
|
<!-- 第四行:应用前端类型(tag 多选;学校模式隐藏) -->
|
||||||
<Row>
|
<Row v-if="!schoolMode">
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
<FormItem label="应用前端类型:">
|
<FormItem label="应用前端类型:">
|
||||||
<Select v-model="form.clients" multiple transfer placeholder="请选择前端类型" class="client-select">
|
<Select v-model="form.clients" multiple transfer placeholder="请选择前端类型" class="client-select">
|
||||||
@@ -54,9 +61,9 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第五行:Pc应用地址 / Pc应用图标 -->
|
<!-- 第五行:Pc应用地址 / Pc应用图标(学校模式仅保留 Pc应用图标) -->
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="12">
|
<Col span="12" v-if="!schoolMode">
|
||||||
<FormItem label="Pc应用地址:">
|
<FormItem label="Pc应用地址:">
|
||||||
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
|
<Input v-model="form.pcUrl" placeholder="请输入Pc应用地址" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -79,8 +86,8 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第六行:App接口地址 / App应用图标 -->
|
<!-- 第六行:App接口地址 / App应用图标(学校模式隐藏) -->
|
||||||
<Row>
|
<Row v-if="!schoolMode">
|
||||||
<Col span="12">
|
<Col span="12">
|
||||||
<FormItem label="App接口地址:">
|
<FormItem label="App接口地址:">
|
||||||
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
|
<Input v-model="form.appUrl" placeholder="请输入App接口地址" />
|
||||||
@@ -104,8 +111,8 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第七行:公众号应用地址 / 钉钉应用地址 -->
|
<!-- 第七行:公众号应用地址 / 钉钉应用地址(学校模式隐藏) -->
|
||||||
<Row>
|
<Row v-if="!schoolMode">
|
||||||
<Col span="12">
|
<Col span="12">
|
||||||
<FormItem label="公众号应用地址:">
|
<FormItem label="公众号应用地址:">
|
||||||
<Input v-model="form.officialAccountUrl" placeholder="请输入公众号应用地址" />
|
<Input v-model="form.officialAccountUrl" placeholder="请输入公众号应用地址" />
|
||||||
@@ -139,11 +146,14 @@ const props = withDefaults(defineProps<{
|
|||||||
categoryList?: any[];
|
categoryList?: any[];
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
/** 学校管理员模式:仅 名称/描述/Pc图标 三字段可编辑(写学校覆盖表) */
|
||||||
|
schoolMode?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
initForm: () => ({}),
|
initForm: () => ({}),
|
||||||
categoryList: () => [],
|
categoryList: () => [],
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
schoolMode: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'reset-secret']);
|
const emit = defineEmits(['update:modelValue', 'submit', 'cancel', 'reset-secret']);
|
||||||
@@ -202,16 +212,22 @@ function onUploadSuccess(response: any, field: 'pcIcon' | 'appIcon') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 前端类型选项 ---------- */
|
/* ---------- 前端类型选项 ---------- */
|
||||||
|
// value 对齐后端平台枚举 com.yunhai.platform.utils.enums.ClientEnum(位掩码 key):
|
||||||
|
// 1=Pc浏览器 2=移动App 8=微信公众号 32=钉钉;公众号/钉钉地址见表单第七行
|
||||||
const clientOptions = [
|
const clientOptions = [
|
||||||
{ value: 1, label: 'Pc浏览器' },
|
{ value: 1, label: 'Pc浏览器' },
|
||||||
{ value: 2, label: '移动App' },
|
{ value: 2, label: '移动App' },
|
||||||
|
{ value: 8, label: '微信公众号' },
|
||||||
|
{ value: 32, label: '钉钉' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ---------- 校验规则 ---------- */
|
/* ---------- 校验规则(学校模式不校验 categoryId——类型字段不展示且不可改) ---------- */
|
||||||
const rules = {
|
const rules = computed(() => ({
|
||||||
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
|
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();
|
const formRef = ref();
|
||||||
|
|||||||
+57
-22
@@ -18,9 +18,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="search-wrap fix">
|
<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"
|
<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"
|
<Input class="fr" v-model="keyword" placeholder="输入应用名称或appKey筛选" search clearable suffix="ios-search"
|
||||||
style="width:220px" @on-search="onSearch" @on-clear="onSearch" />
|
style="width:220px" @on-search="onSearch" @on-clear="onSearch" />
|
||||||
</div>
|
</div>
|
||||||
@@ -30,11 +36,11 @@
|
|||||||
|
|
||||||
<!-- 容器组件:新增 / 编辑 -->
|
<!-- 容器组件:新增 / 编辑 -->
|
||||||
<AddAppContainer ref="addRef" />
|
<AddAppContainer ref="addRef" />
|
||||||
<EditAppContainer ref="editRef" />
|
<EditAppContainer ref="editRef" :schoolId="currentSchoolId" />
|
||||||
|
|
||||||
<!-- 排序管理 -->
|
<!-- 排序管理 -->
|
||||||
<AppSort v-model="sortModalVisible" :categoryId="dataParams.categoryId" :categoryName="currentCategoryName"
|
<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" />
|
<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 TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
|
||||||
import { OperatorEnum } from '@/types';
|
import { OperatorEnum } from '@/types';
|
||||||
import AppSort from './AppSort.vue';
|
import AppSort from './AppSort.vue';
|
||||||
|
import { useAppSchoolScope } from "@/utils/hook/useAppSchoolScope";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { $utils } = useCurrentInstance();
|
const { $utils } = useCurrentInstance();
|
||||||
|
|
||||||
|
// 学校管理员上下文:多校显示切换下拉,覆盖仅对所选学校生效(默认并集,写操作须先选定学校)
|
||||||
|
const { isSchoolMode, multiSchool, schoolList, currentSchoolId, requireSchool } = useAppSchoolScope();
|
||||||
|
|
||||||
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
||||||
|
|
||||||
const sortModalVisible = ref(false);
|
const sortModalVisible = ref(false);
|
||||||
@@ -79,29 +89,39 @@ function onCategoryClick(id: number | null) {
|
|||||||
tableRef.value?.refreshData();
|
tableRef.value?.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 多校切换:刷新类型与列表 ---------------- */
|
||||||
|
function onSchoolChange() {
|
||||||
|
fetchTypeList();
|
||||||
|
tableRef.value?.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 输入框搜索:仅在触发搜索时提交 ---------------- */
|
/* ---------------- 输入框搜索:仅在触发搜索时提交 ---------------- */
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
dataParams.value.name = keyword.value.trim() || undefined;
|
dataParams.value.name = keyword.value.trim() || undefined;
|
||||||
tableRef.value?.refreshData();
|
tableRef.value?.refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 左侧应用类型 ---------------- */
|
/* ---------------- 左侧应用类型(SCHOOL 模式合并学校私有改名/排序;随学校切换刷新) ---------------- */
|
||||||
const { data: appTypeList, send: fetchTypeList } = useRequest(
|
const appTypeList = ref<any[]>([]);
|
||||||
Apis.AppSettings.appTypeList({
|
async function fetchTypeList() {
|
||||||
transform: (res: any) => {
|
try {
|
||||||
const list: any[] = res ?? [];
|
const res: any = await Apis.AppSettings.appTypeList({
|
||||||
return list.slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
params: { schoolId: currentSchoolId.value ?? undefined },
|
||||||
},
|
cacheFor: null,
|
||||||
cacheFor: null,
|
});
|
||||||
}),
|
appTypeList.value = ((res ?? []) as any[]).slice().sort((a, b) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
||||||
{ initialData: [] }
|
} catch (e: any) {
|
||||||
);
|
console.warn(e?.message || "获取应用类型失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchTypeList();
|
||||||
|
|
||||||
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
|
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
|
||||||
return Apis.AppSettings.appList({
|
return Apis.AppSettings.appList({
|
||||||
params: {
|
params: {
|
||||||
categoryId: dataParams.value.categoryId ?? undefined,
|
categoryId: dataParams.value.categoryId ?? undefined,
|
||||||
name: dataParams.value.name || undefined,
|
name: dataParams.value.name || undefined,
|
||||||
|
schoolId: currentSchoolId.value ?? undefined,
|
||||||
pageIndex: params.pageIndex,
|
pageIndex: params.pageIndex,
|
||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
},
|
},
|
||||||
@@ -141,7 +161,8 @@ const columns = [
|
|||||||
key: "clients",
|
key: "clients",
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (h: any, { row }: any) => {
|
render: (h: any, { row }: any) => {
|
||||||
const map: Record<number, string> = { 1: "Pc浏览器", 2: "移动App" };
|
// value 对齐平台 ClientEnum:1=Pc浏览器 2=移动App 8=微信公众号 32=钉钉
|
||||||
|
const map: Record<number, string> = { 1: "Pc浏览器", 2: "移动App", 8: "微信公众号", 32: "钉钉" };
|
||||||
const arr: number[] = Array.isArray(row.clients) ? row.clients : [];
|
const arr: number[] = Array.isArray(row.clients) ? row.clients : [];
|
||||||
if (!arr.length) return h("span", "-");
|
if (!arr.length) return h("span", "-");
|
||||||
return h("span", arr.map((v) => map[v] ?? v).join("、"));
|
return h("span", arr.map((v) => map[v] ?? v).join("、"));
|
||||||
@@ -153,12 +174,14 @@ const columns = [
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
width: 280,
|
width: 280,
|
||||||
render: (h: any, { row }: any) => {
|
render: (h: any, { row }: any) => {
|
||||||
return [
|
// SCHOOL 模式:仅编辑(名称/描述/图标,写学校覆盖表);模块管理/推送设置/删除全部隐藏
|
||||||
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
|
const actions = [$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row))];
|
||||||
$utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)),
|
if (!isSchoolMode.value) {
|
||||||
$utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)),
|
actions.push($utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)));
|
||||||
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
|
actions.push($utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)));
|
||||||
];
|
actions.push($utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)));
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -195,11 +218,19 @@ function handleAdd() {
|
|||||||
const editRef = ref();
|
const editRef = ref();
|
||||||
|
|
||||||
function handleEdit(row: any) {
|
function handleEdit(row: any) {
|
||||||
|
// 多校管理员:编辑写学校覆盖表,必须先选定学校(后端对模糊写目标直接拒绝)
|
||||||
|
if (!requireSchool()) return;
|
||||||
editRef.value?.open(row.id, row, appTypeList.value, () => {
|
editRef.value?.open(row.id, row, appTypeList.value, () => {
|
||||||
tableRef.value?.reloadData(OperatorEnum.edit);
|
tableRef.value?.reloadData(OperatorEnum.edit);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 排序管理入口(多校须先选定学校) ---------------- */
|
||||||
|
function openSortModal() {
|
||||||
|
if (!requireSchool()) return;
|
||||||
|
sortModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 删除应用 ---------------- */
|
/* ---------------- 删除应用 ---------------- */
|
||||||
async function handleDelete(row: any) {
|
async function handleDelete(row: any) {
|
||||||
await MyConfirm({
|
await MyConfirm({
|
||||||
@@ -245,6 +276,10 @@ const goModuleManage = (row: any) => {
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.school-select {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-scroll {
|
.tree-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -40,9 +40,12 @@ const props = withDefaults(
|
|||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
categoryId: number | null;
|
categoryId: number | null;
|
||||||
categoryName: string;
|
categoryName: string;
|
||||||
|
/** 多校管理员当前所选学校:排序快照仅对该校生效 */
|
||||||
|
schoolId?: number | null;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
categoryName: "",
|
categoryName: "",
|
||||||
|
schoolId: null,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,7 +86,10 @@ async function fetchSortList() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await Apis.AppSettings.appSortList({
|
const res = await Apis.AppSettings.appSortList({
|
||||||
params: { categoryId: props.categoryId },
|
params: {
|
||||||
|
categoryId: props.categoryId,
|
||||||
|
schoolId: props.schoolId ?? undefined,
|
||||||
|
},
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
});
|
});
|
||||||
sortList.value = (res ?? []).slice().sort((a: any, b: any) => (a.sortNo ?? 0) - (b.sortNo ?? 0));
|
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;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
const ids = sortList.value.map((item) => item.id);
|
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("排序保存成功!");
|
Message.success("排序保存成功!");
|
||||||
hasChanged.value = false;
|
hasChanged.value = false;
|
||||||
modalVisible.value = false;
|
modalVisible.value = false;
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppForm v-model="visible" :initForm="detailData" :categoryList="categoryList" :isEdit="true" :loading="submitting"
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import AppForm from "./AppForm.vue";
|
import AppForm from "./AppForm.vue";
|
||||||
import MyConfirm from '@/components/MyConfirm'
|
import MyConfirm from '@/components/MyConfirm'
|
||||||
|
import { UserRoleEnum } from "@/types";
|
||||||
|
import { useDefaultStore } from "@/store";
|
||||||
|
|
||||||
/* ---------- 对外暴露 ---------- */
|
/* ---------- 对外暴露 ---------- */
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const categoryList = ref<any[]>([]);
|
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 detailData = ref<Record<string, any>>({});
|
||||||
const currentId = ref<number | null>(null);
|
const currentId = ref<number | null>(null);
|
||||||
|
|
||||||
@@ -21,12 +29,16 @@ let onSuccessCallback: (() => void) | null = null;
|
|||||||
|
|
||||||
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
||||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
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 }
|
{ initialForm: { id: null, name: '', categoryId: undefined, desc: '', clients: [], pcUrl: '', pcIcon: '', appUrl: '', appIcon: '', officialAccountUrl: '', dingtalkUrl: '' }, resetAfterSubmiting: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onSuccess(() => {
|
onSuccess(() => {
|
||||||
Message.success('编辑应用成功!');
|
Message.success(isSchoolMode ? '保存成功(仅对本校生效)!' : '编辑应用成功!');
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
onSuccessCallback?.();
|
onSuccessCallback?.();
|
||||||
});
|
});
|
||||||
@@ -54,23 +66,33 @@ function open(id: number, row: any, categories: any[], onSuccess?: () => void) {
|
|||||||
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
/* ---------- 提交:由 useForm 接管 loading 与异常处理 ---------- */
|
||||||
async function handleSubmit(formData: Record<string, any>) {
|
async function handleSubmit(formData: Record<string, any>) {
|
||||||
if (!currentId.value) return;
|
if (!currentId.value) return;
|
||||||
form.value = {
|
if (isSchoolMode) {
|
||||||
id: currentId.value,
|
// 学校模式:仅 名称/描述/Pc图标 三字段写学校覆盖表,其余字段一律不提交
|
||||||
name: formData.name,
|
form.value = {
|
||||||
categoryId: formData.categoryId,
|
id: currentId.value,
|
||||||
desc: formData.desc,
|
name: formData.name,
|
||||||
clients: formData.clients,
|
desc: formData.desc,
|
||||||
pcUrl: formData.pcUrl,
|
pcIcon: formData.pcIcon,
|
||||||
pcIcon: formData.pcIcon,
|
};
|
||||||
appUrl: formData.appUrl,
|
} else {
|
||||||
appIcon: formData.appIcon,
|
form.value = {
|
||||||
officialAccountUrl: formData.officialAccountUrl,
|
id: currentId.value,
|
||||||
dingtalkUrl: formData.dingtalkUrl,
|
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();
|
await sendForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await) ---------- */
|
/* ---------- 重置密钥(非表单提交,直接 MyConfirm + await;学校模式入口已隐藏) ---------- */
|
||||||
async function handleResetSecret() {
|
async function handleResetSecret() {
|
||||||
if (!currentId.value) return;
|
if (!currentId.value) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function onTreeData(tree: any[]) {
|
|||||||
|
|
||||||
async function loadUserGroups(userId: number) {
|
async function loadUserGroups(userId: number) {
|
||||||
try {
|
try {
|
||||||
const groupIds: number[] = await Apis.SysUser.getGroups({ pathParams: { userId }, cacheFor: null });
|
const groupIds: number[] = await Apis.SysUser.getUserGroups({ pathParams: { userId }, cacheFor: null });
|
||||||
if (treeData.value?.length) {
|
if (treeData.value?.length) {
|
||||||
initSelected(groupIds || []);
|
initSelected(groupIds || []);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="group-user-tree">
|
<div>
|
||||||
<ul class="opera-ul">
|
<ul class="opera-ul">
|
||||||
<li class="add-node-li" @click="showAddNode">
|
<li class="add-node-li" @click="showAddNode">
|
||||||
<Tooltip content="新增子组" placement="top" transfer>
|
<Tooltip content="新增子组" placement="top" transfer>
|
||||||
@@ -344,13 +344,6 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.group-user-tree {
|
|
||||||
width: 300px !important;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opera-ul {
|
.opera-ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { useDefaultStore } from "@/store";
|
|
||||||
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
|
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
|
||||||
@@ -149,31 +148,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
|
|||||||
|
|
||||||
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }))
|
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }))
|
||||||
|
|
||||||
/* ================= 用户身份(roles[])展示 ================= */
|
|
||||||
// 唯一真源:后端 SysRoleEnum(经 GetSysDict.sysrole 下发),杜绝前端硬编码漂移。
|
|
||||||
// roles 中仅存放 SysRoleEnum 的 key(1=老师 … 7=管理员),不再混用 UserTypeEnum。
|
|
||||||
const SYS_ROLE_MAP_FALLBACK: Record<number, string> = {
|
|
||||||
1: '老师', 2: '校领导', 3: '学生', 4: '家长',
|
|
||||||
5: '局领导', 6: '工作号', 7: '管理员',
|
|
||||||
};
|
|
||||||
const store = useDefaultStore();
|
|
||||||
const userTypeMap = computed<Record<number, string>>(() => {
|
|
||||||
const sysrole = (store.getSysDict as any)?.sysrole;
|
|
||||||
if (Array.isArray(sysrole) && sysrole.length) {
|
|
||||||
const m: Record<number, string> = {};
|
|
||||||
sysrole.forEach((it: any) => { m[Number(it.k)] = it.v; });
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
return SYS_ROLE_MAP_FALLBACK;
|
|
||||||
});
|
|
||||||
function describeRoles(roles: any): string {
|
|
||||||
if (!Array.isArray(roles) || roles.length === 0) return '-';
|
|
||||||
const names = roles
|
|
||||||
.map((r: any) => userTypeMap.value[Number(r)])
|
|
||||||
.filter((n: string) => !!n);
|
|
||||||
return names.length ? names.join('/') : '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================= 表格列 ================= */
|
/* ================= 表格列 ================= */
|
||||||
const columns = [
|
const columns = [
|
||||||
{ type: 'selection', width: 55, align: 'center' },
|
{ type: 'selection', width: 55, align: 'center' },
|
||||||
@@ -182,14 +156,23 @@ const columns = [
|
|||||||
{ title: '真实姓名', key: 'realName', minWidth: 120 },
|
{ title: '真实姓名', key: 'realName', minWidth: 120 },
|
||||||
{ title: '手机号', key: 'phone', minWidth: 140 },
|
{ title: '手机号', key: 'phone', minWidth: 140 },
|
||||||
{
|
{
|
||||||
title: '用户身份', key: 'roles', minWidth: 180,
|
title: '用户身份', key: 'identityNames', minWidth: 180,
|
||||||
render: (h: any, { row }: any) => h('span', describeRoles(row.roles)),
|
// 数据源:后端按页批量带出的主校身份名(is_main 行,roles 列已停写,不再直读旧列)
|
||||||
|
render: (h: any, { row }: any) => h('span', row.identityNames || '-'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态', key: 'disabled', width: 90, align: 'center',
|
title: '状态', key: 'disabled', width: 130, align: 'center',
|
||||||
render: (h: any, { row }: any) =>
|
render: (h: any, { row }: any) => {
|
||||||
h('Tag', { color: row.disabled ? 'default' : 'success' },
|
if (row.disabled) return h('Tag', { color: 'default' }, '禁用');
|
||||||
row.disabled ? '已禁用' : '启用'),
|
if (row.groupStatus === 1) {
|
||||||
|
// 正常 Tag + 转出徽标(view-ui-plus Badge status 模式:圆点 + 文本)
|
||||||
|
return h('div', { style: 'display:inline-flex;align-items:center;gap:6px' }, [
|
||||||
|
h('Tag', { color: 'success' }, '正常'),
|
||||||
|
h('Badge', { props: { status: 'warning', text: '转出' } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return h('Tag', { color: 'success' }, '正常');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '排序号', key: 'sortNo', width: 90, align: 'center',
|
title: '排序号', key: 'sortNo', width: 90, align: 'center',
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ async function handleSubmit(formData: Record<string, any>) {
|
|||||||
/**
|
/**
|
||||||
* 把表单字段映射成 SysUserRequest:
|
* 把表单字段映射成 SysUserRequest:
|
||||||
* - 只提交后端 SysUserRequest 里声明的字段
|
* - 只提交后端 SysUserRequest 里声明的字段
|
||||||
* - 角色仅来自"用户身份"勾选(userType),groupId 由当前所在组回写
|
* - 身份角色来自"用户身份"勾选(userType → roleIds);schoolId 不传,由后端按 groupId 解析落库
|
||||||
|
* - groupId 由当前所在组回写
|
||||||
*/
|
*/
|
||||||
function buildPayload(formData: Record<string, any>) {
|
function buildPayload(formData: Record<string, any>) {
|
||||||
const roles: number[] = (formData.userType || []).map((v: any) => Number(v));
|
const roleIds: number[] = (formData.userType || []).map((v: any) => Number(v));
|
||||||
return {
|
return {
|
||||||
account: formData.account,
|
account: formData.account,
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
@@ -95,7 +96,7 @@ function buildPayload(formData: Record<string, any>) {
|
|||||||
phone: formData.phone || undefined,
|
phone: formData.phone || undefined,
|
||||||
gender: formData.gender,
|
gender: formData.gender,
|
||||||
groupId: currentGroupId.value,
|
groupId: currentGroupId.value,
|
||||||
roles,
|
roleIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ let onSuccessCallback: (() => void) | null = null;
|
|||||||
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
// useForm 托管提交——currentId 是 ref,闭包内 .value 在 sendForm 时取当前值
|
||||||
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
const { form, loading: submitting, send: sendForm, onSuccess, onError } = useForm(
|
||||||
(dto) => Apis.SysUser.edit({ pathParams: { id: currentId.value }, data: dto }),
|
(dto) => Apis.SysUser.edit({ pathParams: { id: currentId.value }, data: dto }),
|
||||||
{ initialForm: { id: null, account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', gender: 0, userType: [] }, resetAfterSubmiting: true }
|
{ initialForm: { id: null, account: '', realName: '', password: '', confirmPassword: '', idcard: '', phone: '', gender: 0, userType: [], schoolId: null }, resetAfterSubmiting: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onSuccess(() => {
|
onSuccess(() => {
|
||||||
@@ -69,9 +69,9 @@ async function open(row: any, groups: any[], curGroupId?: number | null, onSucce
|
|||||||
detailData.value = {};
|
detailData.value = {};
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
|
||||||
// 1) 用户所属组(权威来源 getGroups);失败或为空则用当前组兜底,绝不留空
|
// 1) 用户所属组(权威来源 getUserGroups);失败或为空则用当前组兜底,绝不留空
|
||||||
try {
|
try {
|
||||||
const groupIdsResp = await Apis.SysUser.getGroups({ pathParams: { userId: row.id } });
|
const groupIdsResp = await Apis.SysUser.getUserGroups({ pathParams: { userId: row.id }, cacheFor: null });
|
||||||
const ids: number[] = Array.isArray(groupIdsResp) ? groupIdsResp : [];
|
const ids: number[] = Array.isArray(groupIdsResp) ? groupIdsResp : [];
|
||||||
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
|
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
|
||||||
let groups_: { id: number; name: string }[] = ids.map(id => ({
|
let groups_: { id: number; name: string }[] = ids.map(id => ({
|
||||||
@@ -84,13 +84,13 @@ async function open(row: any, groups: any[], curGroupId?: number | null, onSucce
|
|||||||
userGroups.value = groups_;
|
userGroups.value = groups_;
|
||||||
userGroupIdsArr.value = groups_.map(g => g.id);
|
userGroupIdsArr.value = groups_.map(g => g.id);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// getGroups 不可用(如后端未部署该接口)时,至少展示当前点击的组,避免空白
|
// getUserGroups 不可用(如后端未部署该接口)时,至少展示当前点击的组,避免空白
|
||||||
if (curGroupId != null) {
|
if (curGroupId != null) {
|
||||||
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
|
const groupMap = new Map<number, any>((groupList as any[]).map(g => [g.id, g]));
|
||||||
userGroups.value = [{ id: curGroupId, name: groupMap.get(curGroupId)?.name ?? ('组#' + curGroupId) }];
|
userGroups.value = [{ id: curGroupId, name: groupMap.get(curGroupId)?.name ?? ('组#' + curGroupId) }];
|
||||||
userGroupIdsArr.value = [curGroupId];
|
userGroupIdsArr.value = [curGroupId];
|
||||||
}
|
}
|
||||||
console.warn('[EditUserContainer] getGroups 失败,已用当前组兜底:', e?.message || e);
|
console.warn('[EditUserContainer] getUserGroups 失败,已用当前组兜底:', e?.message || e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 角色属性下拉(相互独立,失败不影响所在组)
|
// 2) 角色属性下拉(相互独立,失败不影响所在组)
|
||||||
@@ -126,10 +126,6 @@ async function open(row: any, groups: any[], curGroupId?: number | null, onSucce
|
|||||||
|
|
||||||
/* ---------- 把行数据映射成表单回填数据 ---------- */
|
/* ---------- 把行数据映射成表单回填数据 ---------- */
|
||||||
function buildInitForm(row: any, userRoleIds: number[], defaultGroupId: number | null = null) {
|
function buildInitForm(row: any, userRoleIds: number[], defaultGroupId: number | null = null) {
|
||||||
const roles: number[] = Array.isArray(row.roles) ? row.roles : [];
|
|
||||||
// 用户身份(业务身份)= 1..7;角色属性已移除(超管手动入库、管理员走勾选)
|
|
||||||
const userType = roles.filter(r => r >= 1 && r <= 7);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
account: row.account || '',
|
account: row.account || '',
|
||||||
@@ -139,7 +135,9 @@ function buildInitForm(row: any, userRoleIds: number[], defaultGroupId: number |
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
gender: row.gender ?? 0,
|
gender: row.gender ?? 0,
|
||||||
userType,
|
// 用户身份回显唯一来源是 GET UserIdentity(UserForm 内部按「所在组」加载):
|
||||||
|
// roles 列已停写,row.roles 为冻结历史值,误用回显会把废弃值固化进权威表
|
||||||
|
userType: [],
|
||||||
appRoles: userRoleIds || [],
|
appRoles: userRoleIds || [],
|
||||||
userGroupId: defaultGroupId ?? null,
|
userGroupId: defaultGroupId ?? null,
|
||||||
};
|
};
|
||||||
@@ -172,9 +170,10 @@ async function handleSubmit(formData: Record<string, any>) {
|
|||||||
* - 只提交后端 SysUserRequest 里声明的字段
|
* - 只提交后端 SysUserRequest 里声明的字段
|
||||||
* - 编辑时不提交 groupId(后端 edit 会保留用户原分组,且 groupId 仅在 Create 分组校验)
|
* - 编辑时不提交 groupId(后端 edit 会保留用户原分组,且 groupId 仅在 Create 分组校验)
|
||||||
* - 编辑时密码留空表示不修改,所以密码字段不传
|
* - 编辑时密码留空表示不修改,所以密码字段不传
|
||||||
|
* - 身份随 Select 当前选中组提交 {schoolId, roleIds}(未修改也提交原值),由后端按校全量覆盖并对齐
|
||||||
*/
|
*/
|
||||||
function buildPayload(formData: Record<string, any>, id: number) {
|
function buildPayload(formData: Record<string, any>, id: number) {
|
||||||
const roles: number[] = (formData.userType || []).map((v: any) => Number(v));
|
const roleIds: number[] = (formData.userType || []).map((v: any) => Number(v));
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
id,
|
id,
|
||||||
@@ -183,7 +182,8 @@ function buildPayload(formData: Record<string, any>, id: number) {
|
|||||||
idcard: formData.idcard || undefined,
|
idcard: formData.idcard || undefined,
|
||||||
phone: formData.phone || undefined,
|
phone: formData.phone || undefined,
|
||||||
gender: formData.gender,
|
gender: formData.gender,
|
||||||
roles,
|
schoolId: formData.schoolId,
|
||||||
|
roleIds,
|
||||||
};
|
};
|
||||||
// 编辑时若填写了密码才提交(密码为空表示不修改)
|
// 编辑时若填写了密码才提交(密码为空表示不修改)
|
||||||
if (formData.password) {
|
if (formData.password) {
|
||||||
|
|||||||
@@ -57,19 +57,19 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第 5 行:所在组(只读)(仅编辑展示,普通单选、默认选中当前组;提交时不带 groupId,不改变所在组) -->
|
<!-- 第 5 行:所在组(仅编辑展示;切换只触发下方「用户身份」回显,不改变实际所在组) -->
|
||||||
<Row v-if="isEdit">
|
<Row v-if="isEdit">
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
<FormItem label="所在组(只读):">
|
<FormItem label="所在组:">
|
||||||
<Select v-model="form.userGroupId" transfer clearable
|
<Select v-model="form.userGroupId" transfer clearable
|
||||||
placeholder="默认选中当前组(提交不改变所在组)">
|
placeholder="切换后回显该组身份(不改变实际所在组)">
|
||||||
<Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" />
|
<Option v-for="g in userGroups" :key="g.id" :value="g.id" :label="g.name" />
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- 第 6 行:用户身份(checkbox 多选,整行) -->
|
<!-- 第 6 行:用户身份(checkbox 多选,整行;编辑时跟随所在组 Select 回显) -->
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
<FormItem label="用户身份:" prop="userType">
|
<FormItem label="用户身份:" prop="userType">
|
||||||
@@ -105,7 +105,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from "vue";
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
import { useDefaultStore } from "@/store";
|
import { useDefaultStore } from "@/store";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
|
||||||
/* ---------- Props / Emits ---------- */
|
/* ---------- Props / Emits ---------- */
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
@@ -145,10 +147,14 @@ const defaultForm = (): Record<string, any> => ({
|
|||||||
userType: [] as number[],
|
userType: [] as number[],
|
||||||
appRoles: [] as number[],
|
appRoles: [] as number[],
|
||||||
userGroupId: null as number | null,
|
userGroupId: null as number | null,
|
||||||
|
schoolId: null as number | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = reactive(defaultForm());
|
const form = reactive(defaultForm());
|
||||||
|
|
||||||
|
// 编辑模式回显到的最新的身份基线(用于「非空清成空」二次确认;随所在组切换同步更新)
|
||||||
|
let baselineUserType: number[] = [];
|
||||||
|
|
||||||
// 外部 initForm 变化时回填
|
// 外部 initForm 变化时回填
|
||||||
watch(() => props.initForm, (val) => {
|
watch(() => props.initForm, (val) => {
|
||||||
if (val && Object.keys(val).length > 0) {
|
if (val && Object.keys(val).length > 0) {
|
||||||
@@ -179,6 +185,24 @@ const userTypeOptions = computed(() => {
|
|||||||
return SYS_ROLE_FALLBACK;
|
return SYS_ROLE_FALLBACK;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ---------- 用户身份回显(唯一来源 GET UserIdentity) ---------- */
|
||||||
|
// 所在组 Select 切换(含弹窗打开时的初始选中)→ 后端解析 schoolId 后回显 {schoolId, roleIds}。
|
||||||
|
// 同一学校多组由后端返回同一 schoolId 与角色并集;前端不感知解析逻辑。
|
||||||
|
watch(() => form.userGroupId, (gid: number | null) => {
|
||||||
|
if (!props.isEdit || gid == null || !form.id) return;
|
||||||
|
Apis.SysUser.userIdentity({ params: { userId: form.id, groupId: gid }, cacheFor: null })
|
||||||
|
.then((resp: any) => {
|
||||||
|
const roleIds = Array.isArray(resp?.roleIds) ? resp.roleIds.map((v: any) => Number(v)) : [];
|
||||||
|
form.schoolId = resp?.schoolId ?? null;
|
||||||
|
form.userType = roleIds;
|
||||||
|
baselineUserType = roleIds;
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
// 全局拦截器已弹窗,这里仅记录日志避免二次弹窗;schoolId 保持 null 时提交会被后端拒绝,不会误清空
|
||||||
|
console.warn('[UserForm] 加载用户身份失败:', e?.message || '未知错误');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* ---------- 校验规则 ---------- */
|
/* ---------- 校验规则 ---------- */
|
||||||
// 自定义确认密码校验:新增必填且必须与密码一致;编辑时两者皆空表示不修改,任一非空则必须一致
|
// 自定义确认密码校验:新增必填且必须与密码一致;编辑时两者皆空表示不修改,任一非空则必须一致
|
||||||
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
||||||
@@ -223,6 +247,14 @@ const validateAccount = (rule: any, value: string, callback: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 用户身份校验拆分:新增强制 ≥1 必填;编辑允许为空(空数组提交 = 清空该校身份,
|
||||||
|
// 否则切到一个尚无身份数据的学校后连改手机号都会被 min:1 卡死)
|
||||||
|
const validateUserType = (rule: any, value: number[], callback: any) => {
|
||||||
|
if (props.isEdit) return callback();
|
||||||
|
if (!value || value.length === 0) return callback(new Error('请选择至少一个用户身份'));
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
// 密码红五星号仅新增显示;校验规则随 isEdit 变化
|
// 密码红五星号仅新增显示;校验规则随 isEdit 变化
|
||||||
const rules = computed(() => ({
|
const rules = computed(() => ({
|
||||||
account: [
|
account: [
|
||||||
@@ -239,7 +271,7 @@ const rules = computed(() => ({
|
|||||||
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
{ pattern: /^1\d{10}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
userType: [
|
userType: [
|
||||||
{ required: true, type: 'array' as const, min: 1, message: '请选择至少一个用户身份', trigger: 'change' },
|
{ required: !props.isEdit, validator: validateUserType, trigger: 'change' },
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -254,6 +286,19 @@ watch(() => form.password, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
|
// 编辑模式身份回显未就绪(schoolId 为空)时拦截:避免在身份数据未加载时提交造成误清空
|
||||||
|
if (props.isEdit && form.schoolId == null) {
|
||||||
|
Message.warning('用户身份数据加载中,请稍候再提交');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 勾选由非空清成空:二次确认(空数组提交 = 清空该校身份)
|
||||||
|
if (props.isEdit && baselineUserType.length > 0 && form.userType.length === 0) {
|
||||||
|
const ok = await MyConfirm({
|
||||||
|
msg: '确定清空该用户在此学校的身份吗?',
|
||||||
|
info: '清空后该校用户将无任何身份',
|
||||||
|
});
|
||||||
|
if (!ok) return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate();
|
await formRef.value?.validate();
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -19,9 +19,8 @@
|
|||||||
<FormItem label="调用参数:">
|
<FormItem label="调用参数:">
|
||||||
<pre class="json-pre">{{ formatJson(row?.requestParams) }}</pre>
|
<pre class="json-pre">{{ formatJson(row?.requestParams) }}</pre>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="数据:">
|
<FormItem label="响应结果:">
|
||||||
<a v-if="row?.id" href="javascript:;" class="download-link" @click.prevent="handleDownload">下载</a>
|
<pre class="json-pre">{{ formatJson(row?.responseData) }}</pre>
|
||||||
<span v-else>-</span>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="创建时间:">
|
<FormItem label="创建时间:">
|
||||||
<span>{{ formatTime(row?.createTime) }}</span>
|
<span>{{ formatTime(row?.createTime) }}</span>
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
|
|
||||||
import Utils from '@/utils';
|
import Utils from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -55,8 +53,6 @@ const show = computed({
|
|||||||
set: val => emit('update:modelValue', val),
|
set: val => emit('update:modelValue', val),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { $conf } = useCurrentInstance();
|
|
||||||
|
|
||||||
const appText = computed(() => {
|
const appText = computed(() => {
|
||||||
if (!props.row?.callerAppName && !props.row?.callerAppKey) return '-';
|
if (!props.row?.callerAppName && !props.row?.callerAppKey) return '-';
|
||||||
const name = props.row.callerAppName || '-';
|
const name = props.row.callerAppName || '-';
|
||||||
@@ -85,11 +81,6 @@ function formatTime(val: any): string {
|
|||||||
if (!val) return '-';
|
if (!val) return '-';
|
||||||
return Utils.dateFormat(val, 'yyyy-MM-dd HH:mm:ss');
|
return Utils.dateFormat(val, 'yyyy-MM-dd HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownload() {
|
|
||||||
if (!props.row?.id) return;
|
|
||||||
location.href = `${$conf.serviceHost}/api/oapilog/Download/${props.row.id}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -124,13 +115,5 @@ function handleDownload() {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-link {
|
|
||||||
color: #577afb;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #3d5afe;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="search-wrap fix">
|
<div class="search-wrap fix">
|
||||||
<Input v-model="dataParams.keyword" placeholder="接口名称 / 应用名" clearable suffix="ios-search"
|
<Input v-model="dataParams.keyword" placeholder="输入接口名/应用名/appKey搜索" clearable suffix="ios-search"
|
||||||
style="width: 220px" @on-search="handleSearch" @on-clear="handleSearch" />
|
style="width: 280px" @on-search="handleSearch" @on-clear="handleSearch" />
|
||||||
|
|
||||||
<Input v-model="dataParams.callerAppKey" placeholder="应用Key" clearable style="width: 130px"
|
|
||||||
@on-enter="handleSearch" @on-clear="handleSearch" />
|
|
||||||
|
|
||||||
<Select v-model="dataParams.success" class="ml-12" clearable placeholder="调用结果" style="width: 120px"
|
<Select v-model="dataParams.success" class="ml-12" clearable placeholder="调用结果" style="width: 120px"
|
||||||
@on-change="handleSearch">
|
@on-change="handleSearch">
|
||||||
@@ -18,7 +15,6 @@
|
|||||||
@on-change="handleSearch">
|
@on-change="handleSearch">
|
||||||
<Option :value="1">重要</Option>
|
<Option :value="1">重要</Option>
|
||||||
<Option :value="2">次要</Option>
|
<Option :value="2">次要</Option>
|
||||||
<Option :value="3">一般</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<DatePicker v-model="startDate" type="date" placeholder="开始时间" format="yyyy-MM-dd" :editable="false"
|
<DatePicker v-model="startDate" type="date" placeholder="开始时间" format="yyyy-MM-dd" :editable="false"
|
||||||
@@ -52,7 +48,6 @@ const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
|
|||||||
|
|
||||||
interface DataParams {
|
interface DataParams {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
callerAppKey?: number;
|
|
||||||
success?: string;
|
success?: string;
|
||||||
logLevel?: number;
|
logLevel?: number;
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
@@ -61,7 +56,6 @@ interface DataParams {
|
|||||||
|
|
||||||
const dataParams = ref<DataParams>({
|
const dataParams = ref<DataParams>({
|
||||||
keyword: undefined,
|
keyword: undefined,
|
||||||
callerAppKey: undefined,
|
|
||||||
success: undefined,
|
success: undefined,
|
||||||
logLevel: undefined,
|
logLevel: undefined,
|
||||||
startTime: undefined,
|
startTime: undefined,
|
||||||
@@ -93,7 +87,6 @@ const tableApi = (params: { pageIndex: number; pageSize: number }) => {
|
|||||||
return (Apis as any).SysOapiLog.oapiLogList({
|
return (Apis as any).SysOapiLog.oapiLogList({
|
||||||
params: clean({
|
params: clean({
|
||||||
keyword: dataParams.value.keyword?.trim() || undefined,
|
keyword: dataParams.value.keyword?.trim() || undefined,
|
||||||
callerAppKey: dataParams.value.callerAppKey,
|
|
||||||
success: dataParams.value.success,
|
success: dataParams.value.success,
|
||||||
logLevel: dataParams.value.logLevel,
|
logLevel: dataParams.value.logLevel,
|
||||||
startTime: dataParams.value.startTime,
|
startTime: dataParams.value.startTime,
|
||||||
|
|||||||
Vendored
+3
@@ -1,5 +1,8 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
// import.meta.env 类型声明:VITE_* 仅供本地开发(.env.dev,vite dev --mode dev)读取;
|
||||||
|
// build(production mode)无 .env/.env.production,VITE_* 恒为 undefined,不会进构建产物。
|
||||||
|
// 线上环境差异由 env-config 运行时配置承担(见 host.config.ts)。
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_BASE_HOST: string;
|
readonly VITE_BASE_HOST: string;
|
||||||
readonly VITE_SSO_HOST: string;
|
readonly VITE_SSO_HOST: string;
|
||||||
|
|||||||
+124
-120
@@ -11,136 +11,140 @@ import presetUno from "@unocss/preset-uno";
|
|||||||
|
|
||||||
import presetRemToPx from "@unocss/preset-rem-to-px";
|
import presetRemToPx from "@unocss/preset-rem-to-px";
|
||||||
|
|
||||||
|
// 构建输出:兼容既有 CI 的 mode 子目录(build:prod → dist/prod、build:ld → dist/ld、build:base → dist/base);
|
||||||
|
// 新命令 `vite build`(默认 production mode)→ dist/。.env.base/ld/prod 均已删除,
|
||||||
|
// 各 mode 产物内容完全相同(VITE_* 恒 undefined),不存在按环境烧值问题。
|
||||||
|
// dev 读 .env.dev(import.meta.env.VITE_*);build 时 .env/.env.production 不存在 → VITE_* 全 undefined,
|
||||||
|
// host.config.ts 自动跳过 → 线上仅走 env-config 接口。注意:勿新建 .env.production / .env。
|
||||||
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
||||||
// 构建输出目录:base -> dist/base, ld -> dist/ld, prod -> dist/prod, 默认 dist/base
|
const subDir = mode === "ld" || mode === "prod" ? mode : mode === "base" ? mode : "";
|
||||||
const subDir = mode === "ld" || mode === "prod" ? mode : "base";
|
const outDir = command === "build" && subDir ? `dist/${subDir}` : "dist";
|
||||||
const outDir = command === "build" ? `dist/${subDir}` : "dist";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
createSvgIconsPlugin({
|
createSvgIconsPlugin({
|
||||||
iconDirs: [resolve(process.cwd(), "src/assets/icons")],
|
iconDirs: [resolve(process.cwd(), "src/assets/icons")],
|
||||||
symbolId: "icon-[dir]-[name]", // symbol的id
|
symbolId: "icon-[dir]-[name]", // symbol的id
|
||||||
inject: "body-last", // 插入的位置
|
inject: "body-last", // 插入的位置
|
||||||
customDomId: "__svg__icons__dom__", // svg的id
|
customDomId: "__svg__icons__dom__", // svg的id
|
||||||
}),
|
}),
|
||||||
viteMockServe({
|
viteMockServe({
|
||||||
mockPath: "./src/assets/mock",
|
mockPath: "./src/assets/mock",
|
||||||
}),
|
}),
|
||||||
ViteAutoImport({
|
ViteAutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
"vue",
|
"vue",
|
||||||
"vue-router",
|
"vue-router",
|
||||||
{
|
{
|
||||||
"view-ui-plus": ["Button", "Table", "Modal", "Message", "Form", "FormItem", "Input", "Select"],
|
"view-ui-plus": ["Button", "Table", "Modal", "Message", "Form", "FormItem", "Input", "Select"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: "alova/client",
|
from: "alova/client",
|
||||||
imports: ["usePagination", "useRequest", "useWatcher", "useForm"],
|
imports: ["usePagination", "useRequest", "useWatcher", "useForm"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dts: "./src/auto-imports.d.ts",
|
dts: "./src/auto-imports.d.ts",
|
||||||
dirs: ["src/api"], // 自动导入 hooks
|
dirs: ["src/api"], // 自动导入 hooks
|
||||||
}),
|
}),
|
||||||
createHtmlPlugin({
|
createHtmlPlugin({
|
||||||
minify: true,
|
minify: true,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
entry: "src/main.ts",
|
entry: "src/main.ts",
|
||||||
template: "index.html",
|
template: "index.html",
|
||||||
filename: "index.html",
|
filename: "index.html",
|
||||||
injectOptions: {
|
injectOptions: {
|
||||||
data: {
|
data: {
|
||||||
title: "用户中心",
|
title: "用户中心",
|
||||||
injectScript: ``,
|
injectScript: ``,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
}),
|
|
||||||
UnoCSS({
|
|
||||||
presets: [
|
|
||||||
presetUno(), // 核心预设(Tailwind风格)
|
|
||||||
presetRemToPx({ baseFontSize: 4 }), // 1单位=1px(去rem化)
|
|
||||||
],
|
|
||||||
shortcuts: {
|
|
||||||
// 布局
|
|
||||||
"flex-center": "flex items-center justify-center",
|
|
||||||
// 常用组件
|
|
||||||
btn: "px-16px py-8px rounded-4px cursor-pointer transition-all duration-200",
|
|
||||||
"btn-primary": "bg-blue-600 text-white hover:bg-blue-700",
|
|
||||||
card: "bg-white rounded-8px shadow-md p-16px",
|
|
||||||
// 文本截断
|
|
||||||
"text-ellipsis": "truncate",
|
|
||||||
"text-multi-ellipsis": "line-clamp-2",
|
|
||||||
},
|
},
|
||||||
theme: {
|
],
|
||||||
fontSize: {
|
}),
|
||||||
xs: "12px",
|
UnoCSS({
|
||||||
sm: "14px",
|
presets: [
|
||||||
base: "16px",
|
presetUno(), // 核心预设(Tailwind风格)
|
||||||
lg: "18px",
|
presetRemToPx({ baseFontSize: 4 }), // 1单位=1px(去rem化)
|
||||||
xl: "20px",
|
],
|
||||||
"2xl": "24px",
|
shortcuts: {
|
||||||
},
|
// 布局
|
||||||
},
|
"flex-center": "flex items-center justify-center",
|
||||||
}),
|
// 常用组件
|
||||||
],
|
btn: "px-16px py-8px rounded-4px cursor-pointer transition-all duration-200",
|
||||||
resolve: {
|
"btn-primary": "bg-blue-600 text-white hover:bg-blue-700",
|
||||||
alias: {
|
card: "bg-white rounded-8px shadow-md p-16px",
|
||||||
"@": path.resolve(__dirname, "./src"),
|
// 文本截断
|
||||||
|
"text-ellipsis": "truncate",
|
||||||
|
"text-multi-ellipsis": "line-clamp-2",
|
||||||
},
|
},
|
||||||
},
|
theme: {
|
||||||
server: {
|
fontSize: {
|
||||||
host: "localhost",
|
xs: "12px",
|
||||||
port: 8130,
|
sm: "14px",
|
||||||
// proxy: {
|
base: "16px",
|
||||||
// // //api是自行设置的请求前缀,任何请求路径以/api开头的请求将被代理到对应的target目标
|
lg: "18px",
|
||||||
// // "/api": {
|
xl: "20px",
|
||||||
// // target: "http://localhost:8064/",
|
"2xl": "24px",
|
||||||
// // // target: "http://debugold.hongyucloud.com/ChildOptCourse",
|
|
||||||
// // changeOrigin: true, //需要代理跨域
|
|
||||||
// // rewrite: (path: any) => path.replace(/^\/api/, "/api/"), //路径重写,把'/api'替换为''
|
|
||||||
// // },
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
// css预处理器
|
|
||||||
css: {
|
|
||||||
// preprocessorOptions: {
|
|
||||||
// scss: {
|
|
||||||
// additionalData: '@import "./src/assets/styles/utils/_varible.scss";',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
base: "./",
|
|
||||||
build: {
|
|
||||||
target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules
|
|
||||||
outDir, // 构建输出目录,根据环境变量区分 base/ld
|
|
||||||
assetsDir: "assets", // 静态资源得存放路径文件名 assets
|
|
||||||
sourcemap: false, //构建后是否生成 source map 文件
|
|
||||||
minify: "esbuild", // 项目压缩 :boolean | 'terser' | 'esbuild'
|
|
||||||
chunkSizeWarningLimit: 1000, //chunk 大小警告的限制(以 kbs 为单位)默认:500
|
|
||||||
cssTarget: "chrome61", //防止 vite 将 rgba() 颜色转化为 #RGBA 十六进制符号的形式 (要兼容的场景是安卓微信中的 webview 时,它不支持 CSS 中的 #RGBA 十六进制颜色符号)
|
|
||||||
rollupOptions: {
|
|
||||||
input: {
|
|
||||||
main: resolve("index.html"),
|
|
||||||
},
|
},
|
||||||
output: {
|
|
||||||
chunkFileNames: "static/js/[name]-[hash].js",
|
|
||||||
entryFileNames: "static/js/[name]-[hash].js",
|
|
||||||
assetFileNames: (assetInfo: any) => {
|
|
||||||
if (assetInfo.type === "asset" && /\.(jpe?g|png|gif|svg)$/i.test(assetInfo.name)) {
|
|
||||||
return "static/img/[name].[hash][ext]";
|
|
||||||
}
|
|
||||||
if (assetInfo.type === "asset" && /\.(ttf|woff|woff2|eot)$/i.test(assetInfo.name)) {
|
|
||||||
return "static/fonts/[name].[hash][ext]";
|
|
||||||
}
|
|
||||||
return "static/[ext]/[name]-[hash].[ext]";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
manualEntrypoint: "index.html",
|
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 8130,
|
||||||
|
// proxy: {
|
||||||
|
// // //api是自行设置的请求前缀,任何请求路径以/api开头的请求将被代理到对应的target目标
|
||||||
|
// // "/api": {
|
||||||
|
// // target: "http://localhost:8064/",
|
||||||
|
// // // target: "http://debugold.hongyucloud.com/ChildOptCourse",
|
||||||
|
// // changeOrigin: true, //需要代理跨域
|
||||||
|
// // rewrite: (path: any) => path.replace(/^\/api/, "/api/"), //路径重写,把'/api'替换为''
|
||||||
|
// // },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
// css预处理器
|
||||||
|
css: {
|
||||||
|
// preprocessorOptions: {
|
||||||
|
// scss: {
|
||||||
|
// additionalData: '@import "./src/assets/styles/utils/_varible.scss";',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
base: "./",
|
||||||
|
build: {
|
||||||
|
target: "modules", //设置最终构建的浏览器兼容目标 //es2015(编译成es5) | modules
|
||||||
|
outDir, // 构建输出目录:兼容 CI 的 dist/{prod|ld|base} 子目录(见文件头注释)
|
||||||
|
assetsDir: "assets", // 静态资源得存放路径文件名 assets
|
||||||
|
sourcemap: false, //构建后是否生成 source map 文件
|
||||||
|
minify: "esbuild", // 项目压缩 :boolean | 'terser' | 'esbuild'
|
||||||
|
chunkSizeWarningLimit: 1000, //chunk 大小警告的限制(以 kbs 为单位)默认:500
|
||||||
|
cssTarget: "chrome61", //防止 vite 将 rgba() 颜色转化为 #RGBA 十六进制符号的形式 (要兼容的场景是安卓微信中的 webview 时,它不支持 CSS 中的 #RGBA 十六进制颜色符号)
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: resolve("index.html"),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
chunkFileNames: "static/js/[name]-[hash].js",
|
||||||
|
entryFileNames: "static/js/[name]-[hash].js",
|
||||||
|
assetFileNames: (assetInfo: any) => {
|
||||||
|
if (assetInfo.type === "asset" && /\.(jpe?g|png|gif|svg)$/i.test(assetInfo.name)) {
|
||||||
|
return "static/img/[name].[hash][ext]";
|
||||||
|
}
|
||||||
|
if (assetInfo.type === "asset" && /\.(ttf|woff|woff2|eot)$/i.test(assetInfo.name)) {
|
||||||
|
return "static/fonts/[name].[hash][ext]";
|
||||||
|
}
|
||||||
|
return "static/[ext]/[name]-[hash].[ext]";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manualEntrypoint: "index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user