HR与福利平台的前端复杂度并不只是增删改查,它横跨多角色用户、敏感薪资数据、可配置福利计划以及审计追踪等要求。用 Vue 3 从零搭建这样一套系统,需要先确定包管理、目录结构、类型体系与代码规范,避免业务膨胀后难以维护。本文会以 Zenefits 类产品为参照,梳理从工程化初始化到核心模块落地的完整方案。

一、项目初始化与工程化基础
第一步是用 pnpm 初始化 monorepo 工作区。HR 平台通常包含员工端、HR 管理端、审批中台和内部组件库,用单仓库管理可以共享类型定义和工具函数。根目录的 pnpm-workspace.yaml 可以这样声明包范围:
packages: - apps/* - packages/*
在 apps 下放置 employee-web 和 admin-web 两个应用,packages 下放置 shared-types、ui-kit 和 form-engine。这样福利计划类型、员工档案接口可以在多个应用间复用。TypeScript 的路径别名建议在每个包的 tsconfig.json 中配置,并通过 project references 关联。根目录还应引入 ESLint、Prettier 和 husky,确保提交前统一代码风格。这里不建议在业务早期过度拆包,但类型包和组件库包从第一天就独立,能显著降低后续迁移成本。
Vue 3 工程化还要确定构建工具与环境变量策略。Vite 是目前主流选择,它的按需编译和插件生态适合多应用场景。环境变量可以用 .env.development 和 .env.production 区分,但 HR 平台通常还有测试与预发布环境,建议使用 .env.staging 并配合 Vite 的 mode 参数。密钥类配置不要写入前端代码,统一通过后端下发或构建时注入非敏感标识。比如权限开关、埋点 ID 等可以放在 import.meta.env 中,敏感接口地址则通过运行时配置拉取。
二、核心业务状态建模与动态权限
HR 系统的状态比普通后台复杂,因为同一份员工数据在福利模块、薪资模块和审批模块中呈现不同视图。推荐使用 Pinia 的 setup store 风格,将领域模型拆分为 employeeStore、benefitStore、payrollStore,而不是一个巨大的 userStore 塞满所有字段。以福利选择为例,每个员工当前年度可选的健康保险、牙科保险、退休金计划需要独立 loading、error 和 selection 状态,便于组件细粒度订阅。
<script setup lang="ts">
import { computed } from 'vue'
import { useBenefitStore } from '@/stores/benefit-store'
const benefitStore = useBenefitStore()
const availablePlans = computed(() => benefitStore.availablePlans)
const currentElection = computed(() => benefitStore.currentElection)
function selectPlan(planId: string) {
benefitStore.selectPlan(planId)
}
</script>
<template>
<section v-if="availablePlans.length">
<article v-for="plan in availablePlans" :key="plan.id">
<h3>{{ plan.name }}</h3>
<button :disabled="currentElection?.planId === plan.id" @click="selectPlan(plan.id)">
选择该计划
</button>
</article>
</section>
</template>
动态权限是 HR 平台最容易失控的部分。员工只能查看自己的薪资单和福利选择,HRBP 可以查看部门内员工,财务角色可以查看全公司薪资但看不到健康档案。建议在路由元信息中定义权限点,并通过全局守卫校验。权限数据在登录后由后端返回,前端存储为 code 集合。路由守卫里用 router.beforeEach 检查 meta.permission,没有权限则跳转到 403。不要在前端写死角色判断,因为 Zenefits 类平台支持自定义角色和字段级权限,最好由后端下发可访问的字段列表,前端表单渲染时根据字段权限动态隐藏或禁用。
字段级权限的实现可以配合自定义指令 v-permission 或组合式函数 useFieldPermission。在福利计划配置页面中,HR 管理员能看到成本分摊比例,普通员工只能看到自己需要支付的部分。这类逻辑如果散落在组件里,很快会变得难以测试。更推荐在 JSON Schema 渲染层做统一过滤,权限服务只负责返回当前用户对每个字段的访问级别,渲染引擎根据级别决定控件只读、隐藏或编辑。
三、动态表单引擎与福利选择场景
Zenefits 的一个核心体验是引导员工完成年度福利登记,其中不同地区的员工看到的保险计划不同,添加配偶或子女时表单也会动态变化。硬编码这些表单不可行,需要构建一个基于 JSON Schema 的动态表单引擎。每个福利产品描述成 schema 节点,包含字段类型、校验规则、依赖关系和默认值。渲染器使用 Vue 3 的 <component> 动态组件和响应式数据收集用户输入。
下面是一段简化 schema 示例,描述健康保险表单中的主投保人和家属信息。它包含一个 select 控件和一个依赖生效的 input 控件,当主投保人填写配偶时,配偶姓名才会显示。
export interface FormSchema {
fields: FieldSchema[]
}
export interface FieldSchema {
name: string
label: string
type: 'text' | 'select' | 'date' | 'number'
required?: boolean
visibleWhen?: {
field: string
equals: string | boolean
}
options?: Array<{ label: string, value: string }>
}
const healthInsuranceSchema: FormSchema = {
fields: [
{
name: 'relationship',
label: '与主投保人关系',
type: 'select',
required: true,
options: [
{ label: '本人', value: 'self' },
{ label: '配偶', value: 'spouse' },
{ label: '子女', value: 'child' }
]
},
{
name: 'dependentName',
label: '家属姓名',
type: 'text',
required: true,
visibleWhen: { field: 'relationship', equals: 'spouse' }
}
]
}
在实际工程中,schema 不应只描述静态表单,还需要支持异步数据源。例如选择地区后,险种列表需要从后端按地区拉取;选择牙科计划时,保费金额要根据家属人数重新计算。此时可以在 schema 中配置 dataSource 字段,渲染器通过依赖注入调用对应的数据加载函数。这样业务规则变化时,只需要修改 schema 或服务端配置,不需要重新发布前端代码。
福利选择的提交涉及多个步骤,用户可能在任意一步保存草稿。工程化上建议将表单数据绑定到 Pinia 中的一份 draft 状态,并在路由离开或浏览器关闭前提示保存。提交前用 VeeValidate 或自研校验器执行前端校验,后端再次校验并返回错误码。这里有一个常见误区:把复杂联动逻辑直接写在 watch 中。当字段变多时,watch 依赖会形成网状结构。更清晰的做法是使用可组合的 selectors 或直接让可见性计算函数接收当前 formData,返回布尔值,保持纯函数特性。
四、构建优化与测试策略
随着 HR 平台不断加入招聘、绩效、考勤等模块,打包体积和首屏速度会迅速膨胀。Vite 默认会预构建依赖,但业务代码仍需按路由拆分。使用动态 import 定义路由组件,可以让员工自助端只加载福利和薪资相关代码,管理端按需加载组织架构和审批流。对于大型表格和图表,建议单独拆包,配合 defineAsyncComponent 和 Suspense 使用。构建分析可以使用 rollup-plugin-visualizer,将体积较大的依赖替换为更轻的替代品,例如用 dayjs 替换 moment。
const EmployeeProfile = () => import('@/views/employee/EmployeeProfile.vue')
const BenefitEnrollment = () => import('@/views/benefits/BenefitEnrollment.vue')
const PayrollDetail = () => import('@/views/payroll/PayrollDetail.vue')
测试是工程化质量的关键。单元测试重点关注 Pinia store 和表单校验逻辑,使用 Vitest 模拟 API 响应,断言状态变更是否符合预期。组件测试可以用 Vue Test Utils 覆盖关键交互,比如选择福利后是否出现家属字段、无权限时按钮是否禁用。端到端测试则用 Playwright 或 Cypress 走完员工登录、选择保险、查看确认页的完整流程。HR 平台的回归风险集中在多角色权限与审批流,建议在 CI 中配置每晚跑一次全量 E2E,避免权限改动影响已有功能。
最后要强调环境与发布策略。前端应用通过 docker 构建并部署到 Kubernetes,使用 CDN 加速静态资源,同时配置回滚机制。因为 HR 平台涉及薪资等敏感信息,构建产物需要启用 SRI 校验,并对第三方依赖做安全审计。工程化不是一次性脚手架,而是把类型、状态、权限、表单和测试串成一条可维护的流水线。Vue 3 的组合式 API 和生态让这些实践容易落地,但真正决定成败的是团队对边界和责任域的持续梳理。