做全球化 HR SaaS 产品,最怕的不是功能多,而是扩张快。Horizons 这类全球雇佣与合规管理平台,往往今天只服务新加坡一个市场,下个季度就要同时支持德国、巴西和日本。每个国家的劳动合同字段、薪资币种、法定假期规则都不一样,如果前端架构在一开始没有做工程化设计,页面代码很快就会变成条件判断的地狱。本文以 Vue 3 为核心,聊聊如何搭建一套能扛住全球扩张的 HR 前端工程体系。

一、项目底座:Vite 与目录结构的工程化设计
Vue 3 配合 Vite 已经是当前主流选择,但对一个多国家的 HR 系统来说,光是脚手架默认结构远远不够。我们建议在最初就按业务域(domain)来划分模块,而不是按页面类型划分。比如把员工管理、合同管理、薪资发放、合规报告各自独立成模块,每个模块内部再细分 components、composables、api 和 locale 目录。这样当新增一个国家时,只需要在对应模块下增加区域性配置,而不是到处修改公共代码。
一个经过实践验证的目录结构大致如下:
src/ ├── modules/ │ ├── employee/ # 员工域 │ │ ├── components/ │ │ ├── composables/ │ │ └── api/ │ ├── contract/ # 合同域 │ └── payroll/ # 薪资域 ├── shared/ # 跨域复用的组件与工具 │ ├── composables/ │ └── ui/ ├── locales/ # 多语言文案 └── bootstrap/ # 应用启动、权限初始化
这种按领域划分的方式有一个明显好处:团队可以按模块并行开发,德国市场的合同流程改造不会干扰日本市场的薪资模块。同时配合 Vite 的环境变量能力,把不同区域的 API 网关地址、功能开关都收敛到 .env 文件中,构建时通过 mode 参数区分,避免代码里散落硬编码的地址。
二、Composition API:把国家差异封装成可组合逻辑
Vue 3 最大的工程价值在于 Composition API,它让跨国家的业务差异可以被抽成独立的 composable。举个例子,不同国家对员工姓名的处理规则完全不同:日语有假名注音字段,德语有性别后缀,中东地区姓名顺序又是反的。如果这些逻辑写死在表单组件里,维护成本会指数级增长。
正确的做法是把这些差异下沉到 composable 层:
// composables/useCountryRules.js
import { computed } from 'vue'
export function useCountryRules(countryCode) {
// 根据国家码返回对应的字段规则配置
const nameFields = computed(() => {
const presets = {
JP: [
{ key: 'lastNameKanji', label: '姓氏(漢字)', required: true },
{ key: 'firstNameKanji', label: '名(漢字)', required: true },
{ key: 'lastNameKana', label: '姓氏注音', required: true }
],
DE: [
{ key: 'firstName', label: 'Vorname', required: true },
{ key: 'lastName', label: 'Nachname', required: true },
{ key: 'title', label: 'Titel', required: false }
],
DEFAULT: [
{ key: 'firstName', required: true },
{ key: 'lastName', required: true }
]
}
return presets[countryCode] || presets.DEFAULT
})
return { nameFields }
}
表单组件只负责渲染,具体渲染哪些字段、校验规则是什么,全部由 useCountryRules 决定。当新增一个国家时,只需要在 presets 里加一个配置项,表单组件一行代码都不用改。这就是 Composition API 在多区域业务中的核心价值:差异被隔离,公共逻辑被复用。
同样的思路可以推广到币种格式化、日期时区处理、税务编号校验等场景。比如薪资展示模块,可以封装一个 useCurrencyFormat,根据租户所在区域自动选择货币符号位置和小数位数,避免在每个页面重复写 Intl.NumberFormat 的调用。
三、国际化不只是翻译:i18n 与多时区的深度处理
提到国际化,很多团队的第一反应是接一个 vue-i18n 就完事了。但在真实的全球 HR 系统里,国际化至少包含三层:文案翻译、格式本地化(日期、数字、货币、姓名顺序)和法律文本管理。第三层最容易被忽视——劳动合同条款在不同法域有强制性的措辞要求,这类文案不能和普通 UI 文案混在一个语言包里,否则法务审核一次文案变更就要翻几千条记录。
推荐的实践是拆分语言包的粒度:
// locales/zh-CN/contract.json —— 法律文本独立管理
{
"contract": {
"employmentClause": {
"JP": "依据日本劳动基准法第...",
"DE": "Gemäß § 611 BGB..."
}
}
}
时区是另一个深坑。HR 系统里的考勤打卡、试用期截止日、发薪日都涉及精确到秒的时间计算。如果全部用本地时间存储,跨时区团队的数据会直接错乱。工程上的标准做法是:所有时间以 UTC 或时间戳形式与后端交互,前端仅在展示层转换为用户所在时区,展示统一交给 Intl.DateTimeFormat 处理,不要手写时区换算逻辑。
此外要特别注意夏令时问题。欧洲和北美的夏令时切换日期不同,考勤报表如果用固定偏移量算时间,一年会错两次。把时区转换交给浏览器原生的 Intl API,是成本最低也最不容易出错的方案。
四、权限体系与多租户架构的前端实现
全球扩张的 HR 产品几乎必然走向多租户:不同客户租用的功能模块不同,同一家客户里 HR 管理员、部门经理、普通员工的权限也不同。前端权限不能只靠隐藏按钮,而要建立完整的路由级、组件级、操作级三层控制。
路由层面,用 Vue Router 的导航守卫做统一拦截:
// router/guards.js
import { usePermissionStore } from '@/stores/permission'
export function setupRouterGuard(router) {
router.beforeEach(async (to) => {
const permissionStore = usePermissionStore()
// 首次进入拉取当前用户的权限点集合
if (!permissionStore.loaded) {
await permissionStore.fetchPermissions()
}
// 路由声明了 meta.permissions 则逐一校验
if (to.meta.permissions) {
const ok = to.meta.permissions.every(p => permissionStore.has(p))
if (!ok) return { name: 'Forbidden' }
}
})
}
组件和操作层面,可以封装一个 v-permission 自定义指令,在元素挂载时判断权限点,没有权限则移除节点或禁用交互。相比在每个组件里写 if 判断,指令方式更统一,也不容易遗漏。权限点的命名建议采用「模块:资源:操作」的三段式,例如 payroll:salary:approve,这样后端权限系统和前端代码能保持同一套语义。
五、构建优化与按需加载策略
当系统膨胀到几十个国家的业务模块后,首屏体积会成为明显的体验瓶颈。Vue 3 工程化中的按需加载要抓住三个点:路由级懒加载、重型组件异步加载、区域性代码按需打包。路由懒加载是最基础的,全部路由组件使用动态 import;区域性代码则可以借助 Vite 的动态 import 加上国家码变量,实现只有进入该国页面时才下载对应模块的 chunk。
同时不要忽略依赖分析。用 rollup-plugin-visualizer 生成构建产物分析报告,经常能发现意外打进包里的重量级库,比如全量引入的 lodash、moment(可用 dayjs 替代)或者没用到的图表库全量注册。对于 echarts 这类图表库,务必按需注册用到的组件,体积通常能减少一半以上。全球用户中不少来自网络条件一般的地区,包体积每减少一百 KB,都是实打实的转化率提升。
最后提一条容易被忽略的经验:给前端加上构建产物指纹与 CDN 缓存策略的同时,也要为静态文案包配置版本化路径。语言包更新频繁但主包不常更新,把二者拆开缓存,可以让文案热更新不必让用户重新下载整个应用。
总结
Vue 3 的工程化在全球 HR 扩张场景下,本质上是把「变化」管理起来:国家差异通过 composable 隔离,文案与法律文本分层管理,权限三层拦截,构建产物按需拆分。这四件事做扎实了,新增一个国家市场的成本就能从几周降到几天。工程化不是一次性搭好的架子,而是随着业务扩张持续演进的能力,建议团队每个季度做一次架构复盘,及时把新出现的痛点收敛到统一的抽象层。