- index.vue 来源切换「系统部门|自定义部门」,SYSTEM 原 DOM 原样 v-if 包装零逻辑改动,学校上下文共用 - CustomDeptPanel/CustomDeptCard:分类增删改排序(非空禁删)、部门增改移启停软删(风险二次确认) - CustomMemberModal:候选走 CandidateUsers(服务端同口径排除学生组),搜索+勾选+批量添加 - apiDefinitions/globals.d.ts 补 17 个 /api 契约(key 与后端 operationId 严格一致) - 遗留:npm run api:gen 待后端可起环境补跑校验
This commit is contained in:
@@ -35,6 +35,23 @@ export default {
|
|||||||
'SysGroup.setIncludeChildren': ['POST', '/api/sysgroup/SetIncludeChildren/{id}'],
|
'SysGroup.setIncludeChildren': ['POST', '/api/sysgroup/SetIncludeChildren/{id}'],
|
||||||
'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
|
'SysGroup.batchAdd': ['POST', '/api/sysgroup/BatchAdd'],
|
||||||
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
|
'SysGroup.sysGroupAdd': ['POST', '/api/sysgroup/Add'],
|
||||||
|
'CustomDeptCategory.customDeptCategoryList': ['GET', '/api/deptcategory/List'],
|
||||||
|
'CustomDeptCategory.customDeptCategoryAdd': ['POST', '/api/deptcategory/Add'],
|
||||||
|
'CustomDeptCategory.customDeptCategoryEdit': ['POST', '/api/deptcategory/Edit/{id}'],
|
||||||
|
'CustomDeptCategory.customDeptCategorySort': ['POST', '/api/deptcategory/Sort'],
|
||||||
|
'CustomDeptCategory.customDeptCategoryDelete': ['POST', '/api/deptcategory/Delete/{id}'],
|
||||||
|
'CustomDept.customDeptList': ['GET', '/api/customdept/List'],
|
||||||
|
'CustomDept.customDeptAdd': ['POST', '/api/customdept/Add'],
|
||||||
|
'CustomDept.customDeptEdit': ['POST', '/api/customdept/Edit/{id}'],
|
||||||
|
'CustomDept.customDeptMove': ['POST', '/api/customdept/Move/{id}'],
|
||||||
|
'CustomDept.customDeptSort': ['POST', '/api/customdept/Sort'],
|
||||||
|
'CustomDept.customDeptDisable': ['POST', '/api/customdept/Disable/{id}'],
|
||||||
|
'CustomDept.customDeptEnable': ['POST', '/api/customdept/Enable/{id}'],
|
||||||
|
'CustomDept.customDeptDelete': ['POST', '/api/customdept/Delete/{id}'],
|
||||||
|
'CustomDept.customDeptMembers': ['GET', '/api/customdept/Members/{id}'],
|
||||||
|
'CustomDept.customDeptCandidateUsers': ['GET', '/api/customdept/CandidateUsers/{id}'],
|
||||||
|
'CustomDept.customDeptAddMembers': ['POST', '/api/customdept/AddMembers/{id}'],
|
||||||
|
'CustomDept.customDeptRemoveMembers': ['POST', '/api/customdept/RemoveMembers/{id}'],
|
||||||
'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}'],
|
||||||
'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
|
'GroupAuthz.setSchoolApp': ['POST', '/api/groupauthz/SetSchoolApp'],
|
||||||
|
|||||||
Vendored
+541
@@ -151,6 +151,56 @@ export interface SysGroupRequest {
|
|||||||
export interface SysGroupIncludeChildrenRequest {
|
export interface SysGroupIncludeChildrenRequest {
|
||||||
includeChildren: boolean;
|
includeChildren: boolean;
|
||||||
}
|
}
|
||||||
|
export interface CustomDeptCategoryRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface CustomDeptCategorySortRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
export interface CustomDeptCategoryVo {
|
||||||
|
id?: number;
|
||||||
|
schoolId?: number;
|
||||||
|
name?: string;
|
||||||
|
sortNo?: number;
|
||||||
|
/** 该分类下有效部门数;>0 时分类不允许删除 */
|
||||||
|
departmentCount?: number;
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
categoryId?: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentMoveRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
categoryId: number;
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentSortRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
categoryId?: number;
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentMemberRequest {
|
||||||
|
schoolId?: number;
|
||||||
|
userIds: number[];
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentMemberVo {
|
||||||
|
userId?: number;
|
||||||
|
account?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
export interface CustomDepartmentVo {
|
||||||
|
id?: number;
|
||||||
|
schoolId?: number;
|
||||||
|
categoryId?: number;
|
||||||
|
categoryName?: string;
|
||||||
|
name?: string;
|
||||||
|
sortNo?: number;
|
||||||
|
/** true=启用 / false=停用(后端 CUSTOM 侧统一输出 boolean) */
|
||||||
|
enabled?: boolean;
|
||||||
|
memberCount?: number;
|
||||||
|
}
|
||||||
export interface PushTestRequest {
|
export interface PushTestRequest {
|
||||||
appId?: number;
|
appId?: number;
|
||||||
interfacePath?: string;
|
interfacePath?: string;
|
||||||
@@ -247,6 +297,10 @@ export interface SysGroupVo {
|
|||||||
name?: string;
|
name?: string;
|
||||||
type?: number;
|
type?: number;
|
||||||
sort?: number;
|
sort?: number;
|
||||||
|
/** 启停状态:0=启用 / 1=停用(存量 null 按启用处理) */
|
||||||
|
status?: number;
|
||||||
|
/** 包含子组:1=含下级部门 / 0=仅本级(仅学校节点有值) */
|
||||||
|
includeChildren?: number;
|
||||||
children?: SysGroupVo[];
|
children?: SysGroupVo[];
|
||||||
}
|
}
|
||||||
export interface AppItem {
|
export interface AppItem {
|
||||||
@@ -1783,6 +1837,493 @@ declare global {
|
|||||||
config?: Config
|
config?: Config
|
||||||
): Alova2Method<SysGroupVo[], 'SysGroup.getSchoolTree', Config>;
|
): Alova2Method<SysGroupVo[], 'SysGroup.getSchoolTree', Config>;
|
||||||
};
|
};
|
||||||
|
CustomDeptCategory: {
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET]
|
||||||
|
*
|
||||||
|
* **path:** /api/deptcategory/List
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Query Parameters**
|
||||||
|
* ```ts
|
||||||
|
* type QueryParameters = {
|
||||||
|
* schoolId?: number
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = Array<CustomDeptCategoryVo>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCategoryList<
|
||||||
|
Config extends Alova2MethodConfig<CustomDeptCategoryVo[]> & {
|
||||||
|
params?: {
|
||||||
|
schoolId?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDeptCategoryVo[], 'CustomDeptCategory.customDeptCategoryList', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/deptcategory/Add
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDeptCategoryRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = CustomDeptCategoryVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCategoryAdd<
|
||||||
|
Config extends Alova2MethodConfig<CustomDeptCategoryVo> & {
|
||||||
|
data: CustomDeptCategoryRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDeptCategoryVo, 'CustomDeptCategory.customDeptCategoryAdd', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/deptcategory/Edit/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDeptCategoryRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = CustomDeptCategoryVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCategoryEdit<
|
||||||
|
Config extends Alova2MethodConfig<CustomDeptCategoryVo> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
data: CustomDeptCategoryRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDeptCategoryVo, 'CustomDeptCategory.customDeptCategoryEdit', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/deptcategory/Sort
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDeptCategorySortRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCategorySort<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
data: CustomDeptCategorySortRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDeptCategory.customDeptCategorySort', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/deptcategory/Delete/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCategoryDelete<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDeptCategory.customDeptCategoryDelete', Config>;
|
||||||
|
};
|
||||||
|
CustomDept: {
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/List
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Query Parameters**
|
||||||
|
* ```ts
|
||||||
|
* type QueryParameters = {
|
||||||
|
* categoryId: number
|
||||||
|
* schoolId?: number
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = Array<CustomDepartmentVo>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptList<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentVo[]> & {
|
||||||
|
params: {
|
||||||
|
categoryId: number;
|
||||||
|
schoolId?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentVo[], 'CustomDept.customDeptList', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Add
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = CustomDepartmentVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptAdd<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentVo> & {
|
||||||
|
data: CustomDepartmentRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentVo, 'CustomDept.customDeptAdd', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Edit/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = CustomDepartmentVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptEdit<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentVo> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
data: CustomDepartmentRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentVo, 'CustomDept.customDeptEdit', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Move/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentMoveRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = CustomDepartmentVo
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptMove<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentVo> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
data: CustomDepartmentMoveRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentVo, 'CustomDept.customDeptMove', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Sort
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentSortRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptSort<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
data: CustomDepartmentSortRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptSort', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Disable/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptDisable<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptDisable', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Enable/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptEnable<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptEnable', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Delete/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptDelete<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptDelete', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/Members/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = Array<CustomDepartmentMemberVo>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptMembers<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentMemberVo[]> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentMemberVo[], 'CustomDept.customDeptMembers', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [GET]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/CandidateUsers/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Query Parameters**
|
||||||
|
* ```ts
|
||||||
|
* type QueryParameters = {
|
||||||
|
* keyword?: string
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = Array<CustomDepartmentMemberVo>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptCandidateUsers<
|
||||||
|
Config extends Alova2MethodConfig<CustomDepartmentMemberVo[]> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
params?: {
|
||||||
|
keyword?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<CustomDepartmentMemberVo[], 'CustomDept.customDeptCandidateUsers', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/AddMembers/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentMemberRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptAddMembers<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
data: CustomDepartmentMemberRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptAddMembers', Config>;
|
||||||
|
/**
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* [POST]
|
||||||
|
*
|
||||||
|
* **path:** /api/customdept/RemoveMembers/{id}
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Request Body**
|
||||||
|
* ```ts
|
||||||
|
* type RequestBody = CustomDepartmentMemberRequest
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ---
|
||||||
|
*
|
||||||
|
* **Response**
|
||||||
|
* ```ts
|
||||||
|
* type Response = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
customDeptRemoveMembers<
|
||||||
|
Config extends Alova2MethodConfig<null> & {
|
||||||
|
pathParams: {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
data: CustomDepartmentMemberRequest;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
config: Config
|
||||||
|
): Alova2Method<null, 'CustomDept.customDeptRemoveMembers', Config>;
|
||||||
|
};
|
||||||
PushSubscription: {
|
PushSubscription: {
|
||||||
/**
|
/**
|
||||||
* ---
|
* ---
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<article class="cdept-card" :class="{ 'is-off': !dept.enabled }">
|
||||||
|
<div class="row-1">
|
||||||
|
<span class="status-dot" :class="{ 'is-off': !dept.enabled }"></span>
|
||||||
|
<h3 class="card-name" :title="dept.name">{{ dept.name }}</h3>
|
||||||
|
<span class="sw" :class="{ 'is-off': !dept.enabled }" :title="dept.enabled ? '点击停用' : '点击启用'"
|
||||||
|
@click="emit('toggle', dept)"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row-2">
|
||||||
|
<span class="sub">{{ memberText }}</span>
|
||||||
|
<span class="ops">
|
||||||
|
<i class="op" :class="{ 'is-disabled': !canMoveUp }" title="上移" @click="emit('moveUp')">↑</i>
|
||||||
|
<i class="op" :class="{ 'is-disabled': !canMoveDown }" title="下移" @click="emit('moveDown')">↓</i>
|
||||||
|
<a class="link" @click="emit('members', dept)">成员</a>
|
||||||
|
<a class="link" @click="emit('edit', dept)">编辑</a>
|
||||||
|
<a class="link danger" @click="emit('remove', dept)">删除</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { CustomDepartmentVo } from "@/api/globals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CUSTOM 部门卡片:与 SYSTEM 的 DeptCard 同款外观,但字段来自 ub_custom_department。
|
||||||
|
* 停用在管理端照常可见(灰态),软删的部门由后端列表过滤、不会走到这里。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{
|
||||||
|
dept: CustomDepartmentVo;
|
||||||
|
canMoveUp?: boolean;
|
||||||
|
canMoveDown?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "toggle", dept: CustomDepartmentVo): void;
|
||||||
|
(e: "edit", dept: CustomDepartmentVo): void;
|
||||||
|
(e: "remove", dept: CustomDepartmentVo): void;
|
||||||
|
(e: "members", dept: CustomDepartmentVo): void;
|
||||||
|
(e: "moveUp"): void;
|
||||||
|
(e: "moveDown"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const memberText = computed(() => {
|
||||||
|
const count = Number(props.dept?.memberCount ?? 0);
|
||||||
|
if (!props.dept?.enabled) return count ? `已停用 · ${count} 人` : "已停用 · 无成员";
|
||||||
|
return count ? `${count} 人` : "无成员(可先建部门再配人)";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.cdept-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 13px 14px;
|
||||||
|
background: var(--panel, #fff);
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
border-radius: var(--r, 10px);
|
||||||
|
box-shadow: 0 1px 2px rgba(30, 38, 53, .03);
|
||||||
|
transition: border-color .15s, box-shadow .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdept-card:hover {
|
||||||
|
border-color: #dbe2fb;
|
||||||
|
box-shadow: 0 6px 18px rgba(30, 38, 53, .07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdept-card.is-off {
|
||||||
|
background: #fbfcfd;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-1 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
flex: 0 0 6px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--green, #20a162);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot.is-off {
|
||||||
|
background: #cbd2df;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text, #1e2635);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cdept-card.is-off .card-name {
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 26px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 9px;
|
||||||
|
border-top: 1px dashed var(--line-2, #f0f3f9);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-2 .sub {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link.danger {
|
||||||
|
color: #e54545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-2, #5a6478);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op:hover {
|
||||||
|
background: #e8ecfa;
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.op.is-disabled {
|
||||||
|
color: #d3d9e6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 启停开关(与 SYSTEM 卡片同一套视觉) */
|
||||||
|
.sw {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--brand, #577afb);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 18px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(30, 38, 53, .22);
|
||||||
|
transition: left .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw.is-off {
|
||||||
|
background: #d3d9e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw.is-off::after {
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
<template>
|
||||||
|
<div class="custom-panel">
|
||||||
|
<Alert v-if="!schoolId" show-icon type="info" class="state-tip">
|
||||||
|
请在左侧选择学校节点(或切换顶部学校)后再管理自定义部门。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="custom-body">
|
||||||
|
<!-- 左侧:分类是平铺列表,不是树(空 children 也不能挂成树,避免 ± 展开) -->
|
||||||
|
<div class="cat-col">
|
||||||
|
<div class="col-head">
|
||||||
|
<span class="col-title">自定义分类</span>
|
||||||
|
<Button size="small" type="primary" @click="openCategoryCreate">新增分类</Button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loadingCategories" class="col-tip">加载中…</div>
|
||||||
|
<div v-else-if="!categories.length" class="col-tip">
|
||||||
|
暂无自定义分类。分类只是管理容器,不能启停也不能配成员,请先新建一个。
|
||||||
|
</div>
|
||||||
|
<ul v-else class="cat-list">
|
||||||
|
<li v-for="(c, idx) in categories" :key="c.id" class="cat-item"
|
||||||
|
:class="{ 'is-active': c.id === activeCategoryId }" @click="selectCategory(c.id)">
|
||||||
|
<span class="cat-name" :title="c.name">{{ c.name }}</span>
|
||||||
|
<span class="cat-count">{{ c.departmentCount ?? 0 }}</span>
|
||||||
|
<span class="cat-ops" @click.stop>
|
||||||
|
<i class="op" :class="{ 'is-disabled': idx === 0 }" title="上移"
|
||||||
|
@click="moveCategory(idx, -1)">↑</i>
|
||||||
|
<i class="op" :class="{ 'is-disabled': idx === categories.length - 1 }" title="下移"
|
||||||
|
@click="moveCategory(idx, 1)">↓</i>
|
||||||
|
<i class="op" title="改名" @click="openCategoryRename(c)">✎</i>
|
||||||
|
<i class="op" :class="{ 'is-disabled': hasDepartments(c) }"
|
||||||
|
:title="hasDepartments(c) ? '分类下还有部门,不能删除' : '删除分类'"
|
||||||
|
@click="removeCategory(c)">×</i>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:当前分类下的部门卡片 -->
|
||||||
|
<div class="dept-col">
|
||||||
|
<div class="col-head">
|
||||||
|
<span class="col-title">{{ activeCategoryName || '部门' }}</span>
|
||||||
|
<Button size="small" type="primary" :disabled="!activeCategoryId" @click="openDeptCreate">
|
||||||
|
新增部门
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Alert v-if="!activeCategoryId" show-icon type="warning" class="state-tip">
|
||||||
|
请先在左侧选择一个分类,或新建一个分类。
|
||||||
|
</Alert>
|
||||||
|
<div v-else-if="loadingDepartments" class="col-tip">加载中…</div>
|
||||||
|
<div v-else-if="!departments.length" class="col-tip">
|
||||||
|
该分类下暂无部门。部门可先新建再配成员,成员为空时不影响保存。
|
||||||
|
</div>
|
||||||
|
<div v-else class="dept-grid">
|
||||||
|
<CustomDeptCard v-for="(d, idx) in departments" :key="d.id" :dept="d"
|
||||||
|
:can-move-up="idx > 0" :can-move-down="idx < departments.length - 1"
|
||||||
|
@toggle="onToggleDept" @edit="openDeptEdit" @remove="removeDept"
|
||||||
|
@members="openMembers" @move-up="moveDept(idx, -1)" @move-down="moveDept(idx, 1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 名称录入(分类新增/改名、部门新增/改名共用) -->
|
||||||
|
<Modal v-model="nameModal.visible" :title="nameModal.title" :mask-closable="false" width="420">
|
||||||
|
<Form :label-width="72">
|
||||||
|
<FormItem :label="nameModal.kind === 'dept' ? '部门名称' : '分类名称'">
|
||||||
|
<Input v-model="nameModal.name" :placeholder="nameModal.placeholder"
|
||||||
|
maxlength="64" show-word-limit />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem v-if="nameModal.kind === 'dept' && nameModal.isEdit" label="归属分类">
|
||||||
|
<Select v-model="nameModal.categoryId" placeholder="移动到本校其他分类">
|
||||||
|
<Option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</Option>
|
||||||
|
</Select>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="nameModal.visible = false">取消</Button>
|
||||||
|
<Button type="primary" :loading="nameModal.saving" @click="submitNameModal">保存</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<CustomMemberModal v-model:visible="memberModal.visible" :dept="memberModal.dept"
|
||||||
|
@success="loadDepartments" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import Apis from "@/api";
|
||||||
|
import type { CustomDeptCategoryVo, CustomDepartmentVo } from "@/api/globals";
|
||||||
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
|
import CustomDeptCard from "./CustomDeptCard.vue";
|
||||||
|
import CustomMemberModal from "./CustomMemberModal.vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CUSTOM 自定义部门管理面板(部门管理页的第二个来源页签)。
|
||||||
|
* 口径:分类(平铺列表,容器)→ 部门(可启停 / 可配成员 / 软删)→ 成员(本校教职工,服务端排除学生组)。
|
||||||
|
* 学校上下文由父级传入,和 SYSTEM 页签共用,切换来源不改学校。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{ schoolId?: number | null }>();
|
||||||
|
|
||||||
|
const categories = ref<CustomDeptCategoryVo[]>([]);
|
||||||
|
const departments = ref<CustomDepartmentVo[]>([]);
|
||||||
|
const activeCategoryId = ref<number | null>(null);
|
||||||
|
const loadingCategories = ref(false);
|
||||||
|
const loadingDepartments = ref(false);
|
||||||
|
|
||||||
|
const activeCategory = computed(() => categories.value.find((c) => c.id === activeCategoryId.value) || null);
|
||||||
|
const activeCategoryName = computed(() => activeCategory.value?.name || "");
|
||||||
|
|
||||||
|
const nameModal = reactive({
|
||||||
|
visible: false,
|
||||||
|
saving: false,
|
||||||
|
kind: "category" as "category" | "dept",
|
||||||
|
isEdit: false,
|
||||||
|
id: null as number | null,
|
||||||
|
name: "",
|
||||||
|
categoryId: null as number | null,
|
||||||
|
title: "",
|
||||||
|
placeholder: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberModal = reactive({ visible: false, dept: null as CustomDepartmentVo | null });
|
||||||
|
|
||||||
|
/* ================= 数据加载 ================= */
|
||||||
|
|
||||||
|
async function loadCategories(keepActive = true) {
|
||||||
|
if (!props.schoolId) {
|
||||||
|
categories.value = [];
|
||||||
|
activeCategoryId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingCategories.value = true;
|
||||||
|
try {
|
||||||
|
const rows = (await Apis.CustomDeptCategory.customDeptCategoryList({
|
||||||
|
params: { schoolId: props.schoolId },
|
||||||
|
cacheFor: null,
|
||||||
|
})) as CustomDeptCategoryVo[];
|
||||||
|
categories.value = rows || [];
|
||||||
|
const stillThere = categories.value.some((c) => c.id === activeCategoryId.value);
|
||||||
|
if (!keepActive || !stillThere) {
|
||||||
|
activeCategoryId.value = categories.value[0]?.id ?? null;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// 失败提示已由全局 alova 拦截器统一弹出,此处只记日志避免二次弹窗
|
||||||
|
console.warn(e?.message || "加载自定义分类失败");
|
||||||
|
categories.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingCategories.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDepartments() {
|
||||||
|
if (!props.schoolId || !activeCategoryId.value) {
|
||||||
|
departments.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingDepartments.value = true;
|
||||||
|
try {
|
||||||
|
const rows = (await Apis.CustomDept.customDeptList({
|
||||||
|
params: { schoolId: props.schoolId, categoryId: activeCategoryId.value },
|
||||||
|
cacheFor: null,
|
||||||
|
})) as CustomDepartmentVo[];
|
||||||
|
departments.value = rows || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "加载自定义部门失败");
|
||||||
|
departments.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingDepartments.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(id: number | null) {
|
||||||
|
activeCategoryId.value = id;
|
||||||
|
loadDepartments();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.schoolId, () => {
|
||||||
|
activeCategoryId.value = null;
|
||||||
|
departments.value = [];
|
||||||
|
loadCategories(false);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
watch(activeCategoryId, () => loadDepartments());
|
||||||
|
|
||||||
|
/* ================= 分类操作 ================= */
|
||||||
|
|
||||||
|
const hasDepartments = (c: CustomDeptCategoryVo) => Number(c.departmentCount || 0) > 0;
|
||||||
|
|
||||||
|
function openCategoryCreate() {
|
||||||
|
Object.assign(nameModal, {
|
||||||
|
kind: "category", isEdit: false, id: null, name: "", categoryId: null,
|
||||||
|
title: "新增分类", placeholder: "如:审核部门 / 事项部门", visible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCategoryRename(c: CustomDeptCategoryVo) {
|
||||||
|
Object.assign(nameModal, {
|
||||||
|
kind: "category", isEdit: true, id: c.id ?? null, name: c.name || "", categoryId: null,
|
||||||
|
title: `重命名分类「${c.name}」`, placeholder: "分类名称", visible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCategory(c: CustomDeptCategoryVo) {
|
||||||
|
if (hasDepartments(c)) {
|
||||||
|
// 服务端同样会拒绝;前端先挡一道并说清原因,避免用户点了才知道
|
||||||
|
Message.warning(`分类「${c.name}」下还有 ${c.departmentCount} 个部门,请先移动或删除这些部门`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MyConfirm({
|
||||||
|
title: "删除分类",
|
||||||
|
msg: `确定删除分类【${c.name}】?`,
|
||||||
|
info: "删除为软删除:分类下的部门必须已先移走,历史引用仍按原名称解析。",
|
||||||
|
async onOk() {
|
||||||
|
await Apis.CustomDeptCategory.customDeptCategoryDelete({ pathParams: { id: c.id as number } });
|
||||||
|
Message.success("已删除");
|
||||||
|
await loadCategories();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上移/下移:整表重排后一次性提交(服务端按传入顺序落 sort_no) */
|
||||||
|
async function moveCategory(idx: number, delta: number) {
|
||||||
|
const target = idx + delta;
|
||||||
|
if (target < 0 || target >= categories.value.length) return;
|
||||||
|
const ordered = [...categories.value];
|
||||||
|
const [row] = ordered.splice(idx, 1);
|
||||||
|
ordered.splice(target, 0, row);
|
||||||
|
categories.value = ordered;
|
||||||
|
try {
|
||||||
|
await Apis.CustomDeptCategory.customDeptCategorySort({
|
||||||
|
data: { schoolId: props.schoolId as number, ids: ordered.map((c) => c.id as number) },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "分类排序保存失败");
|
||||||
|
await loadCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 部门操作 ================= */
|
||||||
|
|
||||||
|
function openDeptCreate() {
|
||||||
|
Object.assign(nameModal, {
|
||||||
|
kind: "dept", isEdit: false, id: null, name: "", categoryId: activeCategoryId.value,
|
||||||
|
title: "新增部门", placeholder: "如:校长室 / 教务处", visible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeptEdit(d: CustomDepartmentVo) {
|
||||||
|
Object.assign(nameModal, {
|
||||||
|
kind: "dept", isEdit: true, id: d.id ?? null, name: d.name || "", categoryId: d.categoryId ?? null,
|
||||||
|
title: `编辑部门「${d.name}」`, placeholder: "部门名称", visible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitNameModal() {
|
||||||
|
const name = nameModal.name.trim();
|
||||||
|
if (!name) {
|
||||||
|
Message.warning("名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nameModal.saving = true;
|
||||||
|
try {
|
||||||
|
if (nameModal.kind === "category") {
|
||||||
|
if (nameModal.isEdit) {
|
||||||
|
await Apis.CustomDeptCategory.customDeptCategoryEdit({
|
||||||
|
pathParams: { id: nameModal.id as number },
|
||||||
|
data: { schoolId: props.schoolId as number, name },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await Apis.CustomDeptCategory.customDeptCategoryAdd({
|
||||||
|
data: { schoolId: props.schoolId as number, name },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nameModal.visible = false;
|
||||||
|
Message.success("已保存");
|
||||||
|
await loadCategories();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nameModal.isEdit) {
|
||||||
|
await Apis.CustomDept.customDeptEdit({
|
||||||
|
pathParams: { id: nameModal.id as number },
|
||||||
|
data: { schoolId: props.schoolId as number, name },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
// 改名与移动分开两个写口:归属分类变了就再走一次 Move
|
||||||
|
const current = departments.value.find((d) => d.id === nameModal.id);
|
||||||
|
if (current && nameModal.categoryId && nameModal.categoryId !== current.categoryId) {
|
||||||
|
await Apis.CustomDept.customDeptMove({
|
||||||
|
pathParams: { id: nameModal.id as number },
|
||||||
|
data: { schoolId: props.schoolId as number, categoryId: nameModal.categoryId },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await Apis.CustomDept.customDeptAdd({
|
||||||
|
data: { schoolId: props.schoolId as number, categoryId: activeCategoryId.value as number, name },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nameModal.visible = false;
|
||||||
|
Message.success("已保存");
|
||||||
|
await Promise.all([loadCategories(), loadDepartments()]);
|
||||||
|
} catch (e: any) {
|
||||||
|
// 同名校验等后端拒绝由全局拦截器弹出,弹窗保持打开让用户改
|
||||||
|
console.warn(e?.message || "保存失败");
|
||||||
|
} finally {
|
||||||
|
nameModal.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToggleDept(d: CustomDepartmentVo) {
|
||||||
|
if (d.enabled) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "停用部门",
|
||||||
|
msg: `确定停用自定义部门【${d.name}】?`,
|
||||||
|
info: "停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」和「办」(已有事项展示、在途流程照常解析)。",
|
||||||
|
async onOk() {
|
||||||
|
await disableDept(d);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enableDept(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableDept(d: CustomDepartmentVo) {
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptDisable({ pathParams: { id: d.id as number } });
|
||||||
|
Message.success("已停用");
|
||||||
|
await loadDepartments();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "停用失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableDept(d: CustomDepartmentVo) {
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptEnable({ pathParams: { id: d.id as number } });
|
||||||
|
Message.success("已启用");
|
||||||
|
await loadDepartments();
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "启用失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDept(d: CustomDepartmentVo) {
|
||||||
|
MyConfirm({
|
||||||
|
title: "删除部门",
|
||||||
|
msg: `确定删除自定义部门【${d.name}】?`,
|
||||||
|
info: "删除是软删除:原 ID 不再复用,已有事项 / 流程的引用仍可解析出名称,但新配置里不再可选。请确认没有业务正在用它。",
|
||||||
|
async onOk() {
|
||||||
|
await doRemoveDept(d);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRemoveDept(d: CustomDepartmentVo) {
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptDelete({ pathParams: { id: d.id as number } });
|
||||||
|
Message.success("已删除");
|
||||||
|
await Promise.all([loadCategories(), loadDepartments()]);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "删除失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门上移/下移:同分类内整表重排提交 */
|
||||||
|
async function moveDept(idx: number, delta: number) {
|
||||||
|
const target = idx + delta;
|
||||||
|
if (target < 0 || target >= departments.value.length) return;
|
||||||
|
const ordered = [...departments.value];
|
||||||
|
const [row] = ordered.splice(idx, 1);
|
||||||
|
ordered.splice(target, 0, row);
|
||||||
|
departments.value = ordered;
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptSort({
|
||||||
|
data: {
|
||||||
|
schoolId: props.schoolId as number,
|
||||||
|
categoryId: activeCategoryId.value as number,
|
||||||
|
ids: ordered.map((d) => d.id as number),
|
||||||
|
},
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "部门排序保存失败");
|
||||||
|
await loadDepartments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMembers(d: CustomDepartmentVo) {
|
||||||
|
memberModal.dept = d;
|
||||||
|
memberModal.visible = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.custom-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #1e2635);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-tip {
|
||||||
|
padding: 26px 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
border: 1px dashed var(--line, #e9edf5);
|
||||||
|
border-radius: var(--r, 10px);
|
||||||
|
background: #fbfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左列:分类平铺列表 */
|
||||||
|
.cat-col {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
border-radius: var(--r, 8px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text, #1e2635);
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-item:hover {
|
||||||
|
border-color: #dbe2fb;
|
||||||
|
background: #f7f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-item.is-active {
|
||||||
|
border-color: var(--brand, #577afb);
|
||||||
|
background: #f2f5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-count {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-ops {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-2, #5a6478);
|
||||||
|
}
|
||||||
|
|
||||||
|
.op:hover {
|
||||||
|
background: #e8ecfa;
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.op.is-disabled {
|
||||||
|
color: #d3d9e6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op.is-disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: #d3d9e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右列:部门卡片 */
|
||||||
|
.dept-col {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-grid {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<Modal v-model="innerVisible" :title="title" width="720" :mask-closable="false"
|
||||||
|
@on-visible-change="onVisibleChange">
|
||||||
|
<div class="member-body">
|
||||||
|
<!-- 已有成员 -->
|
||||||
|
<div class="pane">
|
||||||
|
<div class="pane-head">
|
||||||
|
<span class="pane-title">当前成员</span>
|
||||||
|
<span class="pane-sub">{{ members.length }} 人</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="loadingMembers" class="pane-tip">加载中…</div>
|
||||||
|
<div v-else-if="!members.length" class="pane-tip">
|
||||||
|
该部门还没有成员。成员只影响「派人」,不影响部门本身的启停与展示。
|
||||||
|
</div>
|
||||||
|
<ul v-else class="pane-list">
|
||||||
|
<li v-for="u in members" :key="u.userId" class="row">
|
||||||
|
<span class="row-name">{{ u.name || u.account }}</span>
|
||||||
|
<span class="row-sub">{{ u.account }}</span>
|
||||||
|
<a class="row-op danger" @click="remove(u)">移除</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 候选教职工 -->
|
||||||
|
<div class="pane">
|
||||||
|
<div class="pane-head">
|
||||||
|
<Input v-model="kw" placeholder="按账号/姓名搜索" clearable search enter-button
|
||||||
|
@on-search="loadCandidates" @on-change="onKwChange" class="kw" />
|
||||||
|
<span class="pane-sub">已选 {{ selectedIds.size }} 人</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="loadingCandidates" class="pane-tip">加载中…</div>
|
||||||
|
<div v-else-if="!candidates.length" class="pane-tip">
|
||||||
|
没有可添加的教职工。候选=本校教职工(系统会自动排除学生组下的人员),且尚未在该部门。
|
||||||
|
</div>
|
||||||
|
<ul v-else class="pane-list">
|
||||||
|
<li v-for="u in candidates" :key="u.userId" class="row">
|
||||||
|
<Checkbox :model-value="selectedIds.has(u.userId as number)"
|
||||||
|
@on-change="(v: any) => toggle(u.userId as number, !!v)">
|
||||||
|
<span class="row-name">{{ u.name || u.account }}</span>
|
||||||
|
<span class="row-sub">{{ u.account }}</span>
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="candidates.length >= CANDIDATE_PAGE_HINT" class="pane-foot">
|
||||||
|
仅显示前 {{ CANDIDATE_PAGE_HINT }} 条,可输入关键字缩小范围。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button @click="innerVisible = false">关闭</Button>
|
||||||
|
<Button type="primary" :loading="saving" :disabled="!selectedIds.size" @click="addSelected">
|
||||||
|
添加所选
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { Message } from "view-ui-plus";
|
||||||
|
import Apis from "@/api";
|
||||||
|
import type { CustomDepartmentMemberVo, CustomDepartmentVo } from "@/api/globals";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义部门成员管理弹窗。
|
||||||
|
* 候选走 {@code /api/customdept/CandidateUsers/{id}}:服务端按「本校教职工 + 排除学生组 + 未在本部门」给列表,
|
||||||
|
* 所以这里不需要(也不能)用组维度的 GroupUsers 当候选源——那会把学生带进来(任务书 D4)。
|
||||||
|
*/
|
||||||
|
const props = defineProps<{ visible: boolean; dept?: CustomDepartmentVo | null }>();
|
||||||
|
const emit = defineEmits<{ "update:visible": [boolean]; success: [] }>();
|
||||||
|
|
||||||
|
/** 后端候选上限 200,达到就提示用关键字筛选 */
|
||||||
|
const CANDIDATE_PAGE_HINT = 200;
|
||||||
|
|
||||||
|
const innerVisible = computed({
|
||||||
|
get: () => props.visible,
|
||||||
|
set: (v) => emit("update:visible", v),
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = computed(() => `部门成员 · ${props.dept?.name || ""}`);
|
||||||
|
|
||||||
|
const members = ref<CustomDepartmentMemberVo[]>([]);
|
||||||
|
const candidates = ref<CustomDepartmentMemberVo[]>([]);
|
||||||
|
const selectedIds = ref<Set<number>>(new Set());
|
||||||
|
const kw = ref("");
|
||||||
|
const loadingMembers = ref(false);
|
||||||
|
const loadingCandidates = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const deptId = computed(() => props.dept?.id ?? null);
|
||||||
|
|
||||||
|
async function loadMembers() {
|
||||||
|
if (!deptId.value) return;
|
||||||
|
loadingMembers.value = true;
|
||||||
|
try {
|
||||||
|
members.value = (await Apis.CustomDept.customDeptMembers({
|
||||||
|
pathParams: { id: deptId.value },
|
||||||
|
cacheFor: null,
|
||||||
|
})) as CustomDepartmentMemberVo[] || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "加载部门成员失败");
|
||||||
|
members.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingMembers.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCandidates() {
|
||||||
|
if (!deptId.value) return;
|
||||||
|
loadingCandidates.value = true;
|
||||||
|
try {
|
||||||
|
candidates.value = (await Apis.CustomDept.customDeptCandidateUsers({
|
||||||
|
pathParams: { id: deptId.value },
|
||||||
|
params: { keyword: kw.value.trim() || undefined },
|
||||||
|
cacheFor: null,
|
||||||
|
})) as CustomDepartmentMemberVo[] || [];
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "加载候选教职工失败");
|
||||||
|
candidates.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingCandidates.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKwChange() {
|
||||||
|
// clearable 清空时也当一次搜索
|
||||||
|
if (!kw.value) loadCandidates();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(userId: number, checked: boolean) {
|
||||||
|
if (checked) selectedIds.value.add(userId);
|
||||||
|
else selectedIds.value.delete(userId);
|
||||||
|
selectedIds.value = new Set(selectedIds.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSelected() {
|
||||||
|
if (!deptId.value || !selectedIds.value.size) return;
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptAddMembers({
|
||||||
|
pathParams: { id: deptId.value },
|
||||||
|
data: { schoolId: props.dept?.schoolId as number, userIds: [...selectedIds.value] },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
Message.success("已添加");
|
||||||
|
selectedIds.value = new Set();
|
||||||
|
await Promise.all([loadMembers(), loadCandidates()]);
|
||||||
|
emit("success");
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "添加成员失败");
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(u: CustomDepartmentMemberVo) {
|
||||||
|
if (!deptId.value || !u.userId) return;
|
||||||
|
try {
|
||||||
|
await Apis.CustomDept.customDeptRemoveMembers({
|
||||||
|
pathParams: { id: deptId.value },
|
||||||
|
data: { schoolId: props.dept?.schoolId as number, userIds: [u.userId] },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
Message.success("已移除");
|
||||||
|
await Promise.all([loadMembers(), loadCandidates()]);
|
||||||
|
emit("success");
|
||||||
|
} catch (e: any) {
|
||||||
|
console.warn(e?.message || "移除成员失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
members.value = [];
|
||||||
|
candidates.value = [];
|
||||||
|
selectedIds.value = new Set();
|
||||||
|
kw.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibleChange(v: boolean) {
|
||||||
|
if (!v) return;
|
||||||
|
reset();
|
||||||
|
loadMembers();
|
||||||
|
loadCandidates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 外部直接改 visible 时同步一次
|
||||||
|
watch(() => props.visible, (v) => {
|
||||||
|
if (v) onVisibleChange(true);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.member-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
height: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, #1e2635);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-sub {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kw {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
list-style: none;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--line, #e9edf5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-tip {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
border: 1px dashed var(--line, #e9edf5);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fbfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-foot {
|
||||||
|
padding-top: 6px;
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 4px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #272829;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-op {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-op.danger {
|
||||||
|
color: #e54545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,12 +17,21 @@
|
|||||||
<Select v-if="multiSchool" v-model="currentSchoolId" size="small" class="ml-8 school-select">
|
<Select v-if="multiSchool" v-model="currentSchoolId" size="small" class="ml-8 school-select">
|
||||||
<Option v-for="s in schoolOptions" :key="s.id" :value="s.id">{{ s.name }}</Option>
|
<Option v-for="s in schoolOptions" :key="s.id" :value="s.id">{{ s.name }}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
<span class="inc-ctrl ml-auto" title="打开后列出该校下所有层级的部门" @click="toggleInclude">
|
<!-- 来源切换:只切右侧管理视图,学校上下文共用,不改底层组织树 -->
|
||||||
|
<div class="source-tabs ml-auto">
|
||||||
|
<span class="source-tab" :class="{ 'is-active': source === 'SYSTEM' }"
|
||||||
|
@click="source = 'SYSTEM'">系统部门 SYSTEM</span>
|
||||||
|
<span class="source-tab" :class="{ 'is-active': source === 'CUSTOM' }"
|
||||||
|
@click="source = 'CUSTOM'">自定义部门 CUSTOM</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="source === 'SYSTEM'" class="inc-ctrl" title="打开后列出该校下所有层级的部门"
|
||||||
|
@click="toggleInclude">
|
||||||
<span class="inc-label">包含子组</span>
|
<span class="inc-label">包含子组</span>
|
||||||
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
<i class="sw-mini" :class="{ 'is-on': includeOn }"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="source === 'SYSTEM'">
|
||||||
<Alert show-icon type="success" class="usage-note">
|
<Alert show-icon type="success" class="usage-note">
|
||||||
<b>设置说明:</b>
|
<b>设置说明:</b>
|
||||||
停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析);
|
停用只影响「选」(新建事项 / 流程绑定时选不到),不影响「看」(已有事项展示)与「办」(在途流程照常解析);
|
||||||
@@ -39,6 +48,9 @@
|
|||||||
<DeptCard v-for="d in deptList" :key="d.id" :dept="d" :include-children="includeOn"
|
<DeptCard v-for="d in deptList" :key="d.id" :dept="d" :include-children="includeOn"
|
||||||
@toggle="onToggle" />
|
@toggle="onToggle" />
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CustomDeptPanel v-else :school-id="currentSchoolId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,6 +63,7 @@ import Apis from "@/api";
|
|||||||
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
import GroupTreeSearchPanel from "@/components/GroupTreeSearchPanel.vue";
|
||||||
import MyConfirm from "@/components/MyConfirm";
|
import MyConfirm from "@/components/MyConfirm";
|
||||||
import DeptCard from "./DeptCard.vue";
|
import DeptCard from "./DeptCard.vue";
|
||||||
|
import CustomDeptPanel from "./CustomDeptPanel.vue";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { useDefaultStore } from "@/store";
|
import { useDefaultStore } from "@/store";
|
||||||
import { UserRoleEnum } from "@/types";
|
import { UserRoleEnum } from "@/types";
|
||||||
@@ -73,6 +86,8 @@ const roleReady = computed(() => store.roleLoaded);
|
|||||||
|
|
||||||
const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
|
const refTreePanel = ref<InstanceType<typeof GroupTreeSearchPanel>>();
|
||||||
const selectedNode = ref<any>({});
|
const selectedNode = ref<any>({});
|
||||||
|
/** 部门来源页签:SYSTEM=原组织树视图(行为零改动);CUSTOM=学校自建分类/部门 */
|
||||||
|
const source = ref<"SYSTEM" | "CUSTOM">("SYSTEM");
|
||||||
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
|
/** 全量组树:右侧部门数据一律从这里按 id 回查真实 children */
|
||||||
const fullTree = ref<any[]>([]);
|
const fullTree = ref<any[]>([]);
|
||||||
/** 当前学校:超管由树选中,学校管理员默认取可管理学校 */
|
/** 当前学校:超管由树选中,学校管理员默认取可管理学校 */
|
||||||
@@ -167,6 +182,8 @@ async function toggleInclude() {
|
|||||||
school.includeChildren = next ? 1 : 0;
|
school.includeChildren = next ? 1 : 0;
|
||||||
Message.success(next ? "已包含下级部门" : "仅显示一级部门");
|
Message.success(next ? "已包含下级部门" : "仅显示一级部门");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
// 全局 alova 拦截器(alova-iview 的 onBizError/onHttpError)已统一 Message.error,此处只记日志避免二次弹窗;
|
||||||
|
// 本地字段只在写成功后改,失败即自然停在原值,无需回滚
|
||||||
console.warn(e?.message || "设置包含子组失败");
|
console.warn(e?.message || "设置包含子组失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,6 +300,34 @@ async function loadSchoolScope() {
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 来源切换:同页两个视图,不新增菜单项 */
|
||||||
|
.source-tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
background: #f2f4f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tab {
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-2, #5a6478);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tab.is-active {
|
||||||
|
background: var(--panel, #fff);
|
||||||
|
color: var(--brand, #577afb);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 1px 2px rgba(30, 38, 53, .08);
|
||||||
|
}
|
||||||
|
|
||||||
/* 右侧卡片面板撑满剩余高度(与其他页面一致),内容超出时在面板内滚动 */
|
/* 右侧卡片面板撑满剩余高度(与其他页面一致),内容超出时在面板内滚动 */
|
||||||
/* 面板内顶部说明条的间距(外观完全交给 iView Alert 自身,不覆写配色) */
|
/* 面板内顶部说明条的间距(外观完全交给 iView Alert 自身,不覆写配色) */
|
||||||
.usage-note {
|
.usage-note {
|
||||||
|
|||||||
Reference in New Issue
Block a user