全球HR平台的界面看似以表格和表单为主,但真正决定系统成败的是多法域适配、角色隔离和跨模块状态流转。Vue 3 的组合式API让这些能力可以被封装成可复用的函数,而工程化手段则决定项目能否随业务扩张持续演进。类似 Velocity Global 这样的全球雇佣与薪酬管理产品,前端需要同时服务 HR、员工、供应商与管理员,每个角色看到的流程、字段、校验规则都不同。

一、按业务域拆分模块与路由骨架
全球HR产品通常包含入职、合同、薪酬、福利、合规、报销等多个业务域。若将所有页面放在单一目录下,路由文件会迅速膨胀到数千行,任何一次改动都可能触发大范围回归。更合理的做法是采用模块化目录结构,每个业务域独立维护自己的路由、状态、API 封装和组件。Vue 3 的组合式 API 允许在模块入口处集中导出安装函数,再通过主应用统一注册。
在路由层面,可以将每个模块定义为一个 RouteRecordRaw 数组,并在主路由文件中使用扩展运算符合并。这样新增一个国家或产品线时,只需要创建新模块并注册,不需要修改已有核心文件。配合动态导入,每个模块的代码只有在访问对应路由时才会加载,从源头控制首屏体积。对于多租户场景,还可以在路由守卫中根据租户能力裁剪可见路由。
// modules/onboarding/router.ts
import type { RouteRecordRaw } from 'vue-router'
export const onboardingRoutes: RouteRecordRaw[] = [
{
path: '/onboarding',
component: () => import('./layouts/OnboardingLayout.vue'),
children: [
{
path: '',
name: 'OnboardingHome',
component: () => import('./views/OnboardingHome.vue'),
meta: { titleKey: 'nav.onboarding', permission: 'onboarding:view' }
},
{
path: 'case/:caseId',
name: 'OnboardingCaseDetail',
component: () => import('./views/OnboardingCaseDetail.vue'),
meta: { titleKey: 'nav.onboardingCase', permission: 'onboarding:case:view' }
}
]
}
]
模块内部还可以继续拆分领域层、应用层与展示层。领域层负责与后端 API 交互并封装实体类型,应用层负责状态编排和用例,展示层只关心渲染与用户操作。这种分层在 HR 系统中尤其重要,因为工资计算、合同生成和合规校验往往有多套后端服务,前端需要隔离这些变化对界面的影响。
二、国际化与本地化规则引擎
全球HR方案的多语言不只是翻译界面文案,还涉及数字格式、货币符号、日期时区、姓名顺序、地址格式和法定节假日。vue-i18n 是 Vue 3 生态中最成熟的国际化方案,它支持组合式 API 的 useI18n 调用,可以在任意 setup 函数中访问翻译函数和当前语言环境。工程化的关键在于把语言包按模块拆分,避免一次性加载上百个 JSON 文件。
例如,入职模块的语言包可以单独维护在 modules/onboarding/locales/en.json 和 zh-CN.json 中,主应用在注册模块时调用 i18n.global.mergeLocaleMessage 合并。语言切换时,如果目标语言包尚未加载,可以先用默认语言渲染,再异步拉取补充。对于日期和货币,建议基于 Intl.NumberFormat 和 Intl.DateTimeFormat 封装统一的格式化工具,而不是在每个组件里写死规则。这样当业务扩展到新的国家时,只需追加 locale 数据而不是修改组件代码。
// modules/onboarding/locales/loader.ts
import { i18n } from '@/plugins/i18n'
export async function loadOnboardingLocale(locale: string): Promise<void> {
if (i18n.global.availableLocales.includes(locale + '-onboarding')) {
return
}
const messages = await import(`./${locale}.json`)
i18n.global.mergeLocaleMessage(locale, messages.default)
i18n.global.availableLocales.push(locale + '-onboarding')
}
在模板中,普通文案使用 t 函数即可,但如果某个字段标签需要根据雇佣类型变化,例如全职员工显示基础工资,合同工显示日费率,则可以在翻译 key 中加入参数。另一个容易忽略的点是时区转换:HR 操作员可能在纽约,员工在柏林,合同生效时间必须按员工所在地时区展示。可以用一个全局的 useLocalDateTime 组合式函数,同时接收 UTC 时间和目标时区,返回格式化后的本地时间字符串。
三、用 Pinia 组织全球HR核心流程状态
入职流程往往跨越多个页面,从候选人信息、职位审批、背景调查到合同签署,每一步都依赖前几步的数据,并可能因国家不同新增额外步骤。用组件内部状态管理这种流程几乎不可维护,而 Pinia 作为 Vue 3 官方推荐的状态管理库,可以很好地承载跨步骤、跨页面甚至跨模块的状态。核心思路是把每个业务流建模为一个 store,在 store 中维护流程步骤、数据快照、校验结果和提交状态。
以入职申请为例,可以在 store 中使用一个 steps 数组描述步骤元数据,一个 formData 对象保存所有输入,一个 submitState 记录每个步骤的异步状态。当用户在前一步修改数据后,通过 action 触发依赖步骤的重新校验。因为 Pinia 的 state 是响应式的,Vue 组件可以自动更新,配合 shallowRef 缓存大型合同文本,可以避免不必要的深层响应式开销。
// stores/onboardingCase.ts
import { defineStore } from 'pinia'
import type { OnboardingForm } from '@/modules/onboarding/types'
export const useOnboardingCaseStore = defineStore('onboardingCase', {
state: () => ({
currentStep: 0,
formData: {} as OnboardingForm,
submittedSteps: [] as number[],
validationErrors: {} as Record<string, string[]>
}),
getters: {
currentStepId: (state) => state.currentStep + 1,
isCurrentValid: (state) => !state.validationErrors[state.currentStep]?.length
},
actions: {
async submitStep(stepId: number) {
const { validateOnboardingStep } = await import('@/modules/onboarding/api')
const result = await validateOnboardingStep(stepId, this.formData)
this.validationErrors[stepId] = result.errors
if (result.valid) {
this.submittedSteps.push(stepId)
}
}
}
})
对于动态表单,HR 系统经常需要根据国家、雇佣类型、薪资基准自动调整字段。可以将表单定义存储为 schema,例如一个 JSON 结构描述控件类型、校验规则、可见条件。组件层递归渲染 schema,数据层通过 Pinia 统一读写。这样新增一个国家的税务字段时,不需要修改前端组件,只需要新增一份 schema 配置。配合 TypeScript 严格类型,可以提前发现字段名不匹配和缺漏。
四、权限控制:从路由到按钮的纵深隔离
全球HR平台涉及员工薪资、身份证件、银行账户等敏感数据,权限不能只停留在路由层面。一个常见的错误是只做菜单隐藏,但接口仍然可被直接调用,或者用户通过 URL 访问未授权页面。Vue 3 工程化权限建议分四层:路由守卫负责页面级准入,自定义指令负责按钮级控制,API 层负责接口级拦截,数据脱敏函数负责字段级保护。
路由守卫可以在 beforeEach 中读取用户权限列表,并与 route.meta.permission 比较。若用户无权限,则跳转到无权限页面或静默降级。对于按钮,可以注册一个 v-permission 指令,在 mounted 时判断权限,如果缺失则移除元素。需要注意的是,权限判断必须基于后端返回的权限码,而不是前端写死的角色映射,否则一旦角色调整就需要发版。对于字段级权限,例如 HR 可以查看完整银行账号,而员工只能看到后四位,可以封装一个 maskString 工具函数,在渲染前统一处理。
// directives/permission.ts
import type { Directive } from 'vue'
import { useUserStore } from '@/stores/user'
export const vPermission: Directive = {
mounted(el, binding) {
const userStore = useUserStore()
const required = binding.value as string
if (!userStore.hasPermission(required)) {
el.parentNode?.removeChild(el)
}
}
}
在更复杂的全球HR场景中,权限不仅与角色有关,还与数据范围有关。例如一个亚太区 HR 经理只能查看亚太区员工,一个合规官只能访问特定法域的合同。这种基于属性的访问控制可以抽象为 policy 函数,在 store getter 中过滤列表数据。前端做数据范围过滤只是为了优化体验,真正的安全边界必须在后端接口实现,前端所有过滤只应视为展示优化。
五、性能与可观测性工程化
全球HR方案的用户分布在不同网络环境下,首屏性能和交互流畅度直接影响业务效率。工程化性能优化的第一步是识别瓶颈。Vue 3 的开发环境支持 Performance 面板,生产环境建议接入前端监控,收集路由加载时间、组件渲染耗时、API 错误率和 JS 报错。通过 defineAsyncComponent 和路由懒加载,可以把非首屏模块拆成独立 chunk。对于员工列表、合同列表等长列表,使用虚拟滚动只渲染可视区域,可显著降低 DOM 节点数量。
另一个常被忽视的是响应式数据的粒度。如果 store 中保存了上万条员工记录并直接绑定到表格,任何一次局部更新都会触发全量 diff。可以使用 shallowRef 包装大列表,只在列表整体替换时触发更新;需要修改单个员工时,通过不可变更新创建新数组。对于频繁变更的搜索关键字,使用 debounce 减少 watcher 触发次数。这些技巧不需要引入额外库,就能带来明显改善。
// components/EmployeeTable.vue
import { defineAsyncComponent } from 'vue'
const EmployeeRow = defineAsyncComponent(() => import('./EmployeeRow.vue'))
export default {
components: { EmployeeRow }
}
可观测性还包括结构化日志。在全局错误处理器中统一捕获未处理的异常,并附加用户 ID、租户 ID、路由路径和版本号,便于关联后端日志。对 API 请求做统一拦截,记录响应时间超过阈值的慢请求。这样当某个国家的用户反馈页面卡顿时,能快速定位是前端渲染问题还是接口延迟。工程化不是一次性的配置,而是把可观测性作为系统的一部分从第一天开始建设。
从模块化路由、国际化规则、流程状态到权限与性能,Vue 3 为全球HR解决方案提供了完整的工程化拼图。关键在于不把复杂度集中在单个组件或文件中,而是通过清晰的边界和可组合的函数,让跨国业务规则可以被独立维护和验证。只要把握好这些方向,团队就能在扩张到新市场时保持稳定的交付速度。