Ever Gauzy 插件开发实战:深入解析 @gauzy/plugin-job-employee-ui 工作职位员工管理插件
Ever Gauzy 插件开发实战:深入解析 @gauzy/plugin-job-employee-ui 工作职位员工管理插件
本文以 Ever Gauzy 开源商业管理平台(ERP/CRM/HRM/ATS/PM)中的 UI 插件 @gauzy/plugin-job-employee-ui 为核心,完整讲解该插件的安装、构建、测试、发布流程,并结合仓库源码剖析其插件定义、路由权限、页面组件、数据表配置与前后端联动原理。读完本文,你将掌握在 Ever Gauzy 中为「Jobs 员工管理」功能编写、接入与维护一个 Angular UI 插件的完整方法论,也能直接复用到其他 Gauzy 插件(如 Job Search、Job Matching、Job Proposal)的开发实践中。
插件是什么:Jobs 板块下的「Employee」管理界面
@gauzy/plugin-job-employee-ui 是 Ever Gauzy 平台中负责「Job Employee(工作职位员工)」管理界面的前端插件。它的职责可以用一句话概括:在 Gauzy 的 Jobs 导航板块下,为组织管理员提供一个查看与维护「参与职位搜索的员工」的交互页面——包括员工列表、其可用职位数 / 已申请职位数统计、计费费率(Billing Rate)与最低计费费率编辑,以及「是否处于职位搜索状态」的开关切换。
从插件定位上看,它属于 Ever Gauzy 插件体系中的「UI 插件」类型:只负责界面呈现与交互,而业务逻辑由后端 @gauzy/plugin-job-search 中的 EmployeeJobPostController(见 employee-job.controller.ts)提供 REST 接口支撑。插件通过 Nx 生成的 Angular library 结构组织,其工程元数据记录在 project.json 中,tags 标注为 type:plugin 与 type:plugin-extension-point,说明它既是插件又是平台 UI 的扩展点。
该插件的代码量并不大,但麻雀虽小五脏俱全:一个插件定义、一个 NgModule、一套路由配置、一个主页面组件、一个状态编辑组件与一个服务 provider。下面我们沿着官方 README 的操作主线,从安装、构建、测试、发布到源码剖析逐层深入。
安装插件
插件以 npm 包形式分发。在任意 Node.js 项目中安装 @gauzy/plugin-job-employee-ui 只需一行命令(来自 README.md):
npm install @gauzy/plugin-job-employee-ui
# 或使用 yarn
yarn add @gauzy/plugin-job-employee-ui
需要留意的是,插件包的 package.json 声明了若干重要的约束:
peerDependencies要求@angular/common与@angular/core均为21.0.7,因此宿主应用必须运行在匹配的 Angular 21 版本上;dependencies中引用了@gauzy/contracts、@gauzy/ui-core、@gauzy/plugin-ui、@nebular/theme、angular2-smart-table、ngx-permissions、@ngx-translate/core等库,安装时需确保这些依赖能够解析;engines声明了node >= 22、yarn >= 1.22;- 包本身标记为
"private": true,这意味着在仓库内它仅供内部工作区使用,实际对外发布时需由发布流程处理。
构建插件
在 Ever Gauzy 的 monorepo 工作区中,所有包都通过 Nx 统一管理。构建该插件的命令(README 原文):
yarn nx build plugin-job-employee-ui
构建由 @nx/angular:ng-packagr-lite executor 执行,产物输出到 dist/packages/plugins/job-employee-ui(见 project.json)。工程还内置了两个构建配置:
# 开发模式构建(project.json 中的 defaultConfiguration)
yarn nx build plugin-job-employee-ui --configuration=development
# 生产模式构建(使用 tsconfig.lib.prod.json)
yarn nx build plugin-job-employee-ui --configuration=production
package.json 中同步封装了常用脚本:yarn lib:build、yarn lib:build:prod 与 yarn lib:watch(监听模式)。由于构建目标声明了 dependsOn: ["^build"],Nx 会先构建该插件的隐式依赖(contracts、ui-config、ui-core),保证依赖包是最新产物。
运行单元测试
插件的单元测试由 Jest 驱动,命令为:
yarn nx test plugin-job-employee-ui
测试入口配置在 jest.config.ts,并依赖 jest-preset-angular。仓库中现存的测试用例位于 job-employee.component.spec.ts,它演示了如何在非 standalone 环境下测试插件组件:直接将声明了该组件的 JobEmployeeModule 作为测试模块的 imports 导入,并为 EmployeesService、ActivatedRoute(模拟 tabsetId: 'job-employee' 与 dataTableId: 'job-employee-page' 路由数据)提供 mock,最后断言组件能够成功创建:
await TestBed.configureTestingModule({
imports: [JobEmployeeModule],
providers: [
EmployeesService,
{ provide: ActivatedRoute, useValue: { /* tabsetId / dataTableId mock */ } }
],
teardown: { destroyAfterEach: false }
}).compileComponents();
发布插件
构建完成后即可发布。README 给出的发布流程是:进入构建产物目录 dist/packages/plugins/job-employee-ui,然后执行:
npm publish
发布前请确认产物目录中已包含 package.json(ng-packagr 会自动生成)、README.md、类型声明与编译后的 JS 文件。该插件遵循 AGPL-3.0 许可(见 package.json 的 license 字段),对外分发时需遵守相应的许可证条款。
在 Gauzy 应用中接入插件
安装、构建只是把包准备好了,真正让插件「生效」的是在 Gauzy 前端应用(apps/gauzy)的插件配置中心里注册它。应用的 UI 插件配置位于 apps/gauzy/src/plugin-ui.config.ts,它导出一个 PluginUiConfig 对象,其中 plugins 数组即「当前构建激活的插件清单」。
Job Employee 插件被组织在 Jobs 插件组之下,作为 JobsPlugin 的子插件注册,配置代码原文如下:
// apps/gauzy/src/plugin-ui.config.ts
import { JobEmployeePlugin } from '@gauzy/plugin-job-employee-ui';
// ...
export const uiPluginConfig: PluginUiConfig = {
// ... i18n / locale 配置
plugins: [
// ...
JobsPlugin.init({
plugins: [
JobProposalPlugin,
JobEmployeePlugin,
JobSearchPlugin,
JobMatchingPlugin,
JobProposalTemplatePlugin
]
}),
// ...
]
};
这种「父插件 + 子插件」的层级注册方式(JobsPlugin.init({ plugins: <a href="https://link.gitcode.com/i/ad8bb0a013e554b35607689e2cbe033b" target="_blank">...] }))正是 Ever Gauzy 插件体系的组织范式:父插件作为逻辑分组可以没有自己的 module,子插件各自贡献路由、导航与页面,最终在应用启动时被扁平化后统一引导。PluginUiDefinition 类型的完整字段说明见 [packages/plugin-ui/src/lib/plugin-ui.types.ts(包括 id、module、loadModule、location、plugins、routes、navMenu、permissionKeys、dependsOn 等)。
插件定义源码剖析:一份声明式注册清单
插件入口 job-employee-plugin.ts 导出一个常量对象 JobEmployeePlugin,它就是整个插件的「元数据清单」:
export const JobEmployeePlugin: PluginUiDefinition = {
id: 'job-employee',
version: '0.1.0',
location: 'jobs-sections',
module: JobEmployeeModule,
permissionKeys: [PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW],
routes: [JOB_EMPLOYEE_PAGE_ROUTE as PluginRouteInput],
navMenu: [
{
type: 'section',
sectionId: 'jobs',
items: [
{
id: 'jobs-employee',
title: 'Employee',
icon: 'fas fa-user-friends',
link: JOB_EMPLOYEE_PAGE_LINK,
data: {
translationKey: 'MENU.EMPLOYEES',
permissionKeys: [PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW]
}
}
]
}
]
};
逐一解读各字段的含义:
id:插件唯一标识,'job-employee';version:插件版本0.1.0,与 package.json 的版本号保持一致;location:声明该插件挂载的页面路由注册位置,'jobs-sections'与JobsPlugin的父板块位置对应;module:插件的 Angular NgModule 类,即JobEmployeeModule;permissionKeys:插件激活所需权限,这里要求ORG_JOB_EMPLOYEE_VIEW;routes:声明式路由列表,将注册到PageRouteRegistryService;navMenu:导航菜单贡献,在jobssection 下新增一个标题为Employee、图标为fas fa-user-friends、链接指向/pages/jobs/employee的导航项,并附带翻译 keyMENU.EMPLOYEES与权限 key。
这种声明式设计意味着:插件的路由、导航甚至扩展点都可以在「不写一行 Angular 路由代码」的情况下完成注册,真正做到了界面功能按插件粒度插拔。
路由与权限:如何挂到 /pages/jobs/employee
路由配置定义在 job-employee.routes.ts:
export const JOB_EMPLOYEE_PATH = 'employee';
export const JOB_EMPLOYEE_PAGE_LINK = `/pages/jobs/${JOB_EMPLOYEE_PATH}`;
export const JOB_EMPLOYEE_PAGE_ROUTE: PageRouteRegistryConfig = {
location: 'jobs-sections',
path: JOB_EMPLOYEE_PATH,
component: JobEmployeeComponent,
canActivate: [PermissionsGuard],
data: {
tabsetId: 'job-employee',
dataTableId: 'job-employee-page',
selectors: { date: true, employee: true, project: false, team: false },
permissions: {
only: [PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW],
redirectTo: '/pages/jobs/search'
}
}
};
几个值得注意的细节:
- 页面完整地址为
/pages/jobs/employee,在jobs-sections位置注册,渲染组件为JobEmployeeComponent; - 通过
canActivate: [PermissionsGuard]做进入守卫,同时data.permissions.only声明仅拥有ORG_JOB_EMPLOYEE_VIEW权限的用户可访问,无权限时重定向到/pages/jobs/search; data.selectors控制页面顶部的全局筛选器开关:日期与员工筛选开启,项目与团队筛选关闭;tabsetId: 'job-employee'与dataTableId: 'job-employee-page'两个 ID 是页面与 UI 注册中心(PageTabRegistryService、PageDataTableRegistryService)之间的「契约」,组件初始化时从ActivatedRoute快照读取它们(见 job-employee.component.ts)。
后端侧,ORG_JOB_EMPLOYEE_VIEW 权限由 employee-job.controller.ts 中的 GET /employee-job/statistics 接口以 @Permissions(PermissionsEnum.ORG_JOB_EMPLOYEE_VIEW) 注解强制校验,前端权限守卫与后端权限装饰器形成了双层防护。
页面组件:三页签与六列表格的完整实现
JobEmployeeComponent(job-employee.component.ts)是插件的核心视图,扩展自 PaginationFilterBaseComponent,负责分页、筛选与智能表格(angular2-smart-table)的组装。它注册了三个页签(JobSearchTabsEnum):
| 页签 ID | 图标 | 翻译 key | 内容 |
|---|---|---|---|
browse |
globe-2-outline |
JOB_EMPLOYEE.BROWSE |
员工数据表格(tableLayout 模板) |
search |
search-outline |
JOB_EMPLOYEE.SEARCH |
comingSoon 占位模板 |
history |
clock-outline |
JOB_EMPLOYEE.HISTORY |
comingSoon 占位模板 |
数据表注册了 6 列(通过 PageDataTableRegistryService.registerPageDataTableColumn 注册到 job-employee-page 数据表):
- Employee(员工):自定义渲染组件
EmployeeLinksComponent,显示员工姓名、头像与链接,支持排序,编辑组件为EmployeeLinkEditorComponent; - Available Jobs(可用职位数):纯文本列,
valuePrepareFunction将空值归零显示; - Applied Jobs(已申请职位数):同 Available Jobs,空值归零;
- Billing Rate(计费费率):可编辑数字列,使用
NumberEditorComponent,展示时经CurrencyPipe按员工billRateCurrency格式化货币; - Minimum Billing Rate(最低计费费率):可编辑数字列,同样按员工币种格式化;
- Job Search Status(职位搜索状态):自定义渲染组件
ToggleSwitcherComponent开关,编辑组件为JobSearchStatusEditorComponent,切换后调用服务层持久化。
数据加载采用 ServerDataSource(服务端分页数据源),请求端点为 ${API_PREFIX}/employee-job/statistics,即后端 GET /api/employee-job/statistics,并附带 relations: ['user'] 与组织/租户筛选条件:
const whereClause = {
tenantId,
organizationId,
isActive: true,
isArchived: false,
...(this.selectedEmployeeId ? { id: this.selectedEmployeeId } : {}),
...(this.filters.where ? this.filters.where : {})
};
一个值得展开的权限细节:当当前用户没有 CHANGE_SELECTED_EMPLOYEE 权限时,组件会强制把 whereClause.id 设为自己所属员工 ID,即「普通员工只能看到自己的职位统计」,这是多租户 SaaS 场景下典型的行级数据隔离实现。
列表刷新链路也值得学习:组件监听 employees$ 主题(Subject),经过 100ms 防抖后关闭右侧详情抽屉并重新拉取数据;pagination$(分页变化)与 Store 中组织/员工选择变化都会触发 employees$.next(true) 进入刷新链路。
编辑费率与求职状态:前端交互到后端命令的完整链路
行内编辑费率
表格的编辑动作(编辑、保存、取消三组操作按钮)在 _loadSmartTableSettings() 中配置:confirmSave: true 使保存前触发确认回调 onEditConfirm,其实现调用 EmployeesService.updateProfile 更新 minimumBillingRate 与 billRateValue 两个字段(携带 tenantId、organizationId),成功后刷新列表并 confirm.resolve,失败则 confirm.reject。取消编辑则直接调用 smartTableSource.refresh() 回滚行内改动。
该文件内还包含一处颇有价值的实现细节注释:操作按钮图标原本依赖 [innerHTML] 注入字符串,而字符串中的 <nb-icon> 永远不会被实例化,因此组件通过 NbIconLibraries.getSvgIcon() 从已注册的 eva 图标包中直接读出 SVG 内容,并对翻译标签做了 HTML 转义(escapeHtml)与无障碍处理(sr-only 文本),同时配合 sanitizer: { bypassHtml: true } 让 angular2-smart-table 的 DomSanitizer 通道生效——这是插件环境与库渲染机制磨合的典型范例。
切换职位搜索状态
行内开关的底层服务是 job-search-store.service.ts 中的 JobSearchStoreService:
await this._jobService.updateJobSearchStatus(employee.id, {
isJobSearchActive,
organizationId,
tenantId
});
其对应的后端接口为 PUT /employee-job/:id/job-search-status(见 employee-job.controller.ts),需要 ORG_EMPLOYEES_EDIT 或 PROFILE_EDIT 权限,请求体经 EmployeeJobStatisticDTO 校验,最终通过 CQRS 命令总线执行 UpdateEmployeeJobSearchStatusCommand(见 update-employee-job-search-status.command.ts)。服务层在成功与失败时分别弹出 i18n 化的 toast 提示(TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_ACTIVE / EMPLOYEE_JOB_STATUS_INACTIVE)。
JobSearchStatusEditorComponent(job-search-status-editor.component.ts)是 DefaultEditor 的子类,模板直接渲染 ngx-toggle-switcher,并监听 Store.selectedOrganization$ 获取组织上下文,通过 cell.getRow().getData() 取得当前行员工。注意它在组件级 providers 中重新提供了 JobSearchStoreService——源码注释明确指出,这样做的原因是智能表格动态创建的单元格编辑器(如行内开关)的注入器可能看不到模块级 provider(否则会报 NG0201 错误)。
只读详情抽屉
选中行后可通过操作按钮打开右侧详情抽屉(view() / buildViewSections()):以「字段列表」形式纵向展示员工、可用/已申请职位数、格式化后的计费费率与最低费率、职位搜索状态(布尔开关)。抽屉数据与表格列的渲染逻辑(空值归零、货币格式化)保持一致,确保两种视图呈现的信息完全等价。
插件生命周期:声明式注册如何被应用
job-employee.module.ts 中的 JobEmployeeModule 实现了 IOnPluginUiBootstrap 与 IOnPluginUiDestroy 两个生命周期接口。在 ngOnPluginBootstrap() 中调用 applyDeclarativeRegistrations(this._pluginDefinition, { navBuilder, pageRouteRegistry }),一次性把插件定义里的路由与导航项写入 UI 核心服务;模块内通过静态布尔标记 _hasAppliedRegistrations 保证「每次应用生命周期内只注册一次」,ngOnPluginDestroy() 时复位该标记,避免热重载或多实例场景下重复注册。
Angular 的 DI 注入在这里得到了充分运用:inject(LoggerService)(带 JobEmployeeModule 上下文)、inject(NavMenuBuilderService)、inject(PageRouteRegistryService),以及 inject(PLUGIN_DEFINITION, { optional: true })——后者是宿主框架在插件实例化时注入的当前插件定义对象,若为空(如单元测试中未走 PluginUiModule 引导路径)则跳过注册。
插件对外暴露的公共 API 面(index.ts)仅三个导出:JobEmployeePlugin(插件定义)、JobEmployeeModule(Angular 模块)、JobEmployeeComponent(页面组件),保持了「最小公共面」的插件封装原则。
小结:从 README 到生产级插件的完整知识链
回顾整个 @gauzy/plugin-job-employee-ui:
- 工程侧:安装(
npm install/yarn add)、构建(yarn nx build plugin-job-employee-ui)、测试(yarn nx test plugin-job-employee-ui)、发布(cd dist/packages/plugins/job-employee-ui && npm publish)四条命令即可覆盖插件生命周期的全部常规操作; - 集成侧:在 apps/gauzy/src/plugin-ui.config.ts 中通过
JobsPlugin.init({ plugins: [...] })挂载为 Jobs 板块子插件; - 实现侧:声明式插件定义(路由 + 导航 + 权限)+ NgModule 生命周期注册 + 组件页签/数据表注册 + 服务端数据源,构成一套完整的「前端插件 + 后端 REST/CQRS」联动模式。
对于希望在 Ever Gauzy 中扩展职位管理功能(例如为 Browse 页签增加新列、把 Search/History 占位页签实现为真实视图、接入新的职位来源统计)的开发者,本插件即是现成的参考范本:它完整展示了插件如何声明路由、如何受权限门控、如何通过注册中心组装页面元素、以及如何在插件上下文中安全地处理动态单元格编辑器与图标渲染等工程细节。