全球 HR 管理平台(如 Papaya Global)的复杂度远超普通企业后台,它需要承载多国薪资规则、动态组织层级、跨币种结算和本地化合规要求。使用 Vue 3 进行工程化开发时,不能只停留在“组件复用”的层面,而要从构建工具、状态管理、路由权限、国际化乃至部署流水线做整体设计。接下来,我们从实际架构出发,解析如何打造一套可扩展的全球 HR 管理前端。

一、选择 Vue 3 作为底层框架的理由
Vue 3 的组合式 API 将相关逻辑按功能聚合,而不是像 Vue 2 那样按选项强制分散。在 HR 管理系统中,一个员工薪资卡片往往涉及数据拉取、权限校验、汇率换算、表单校验等逻辑,选项式 API 会让人把这些代码拆分到 data、methods、watch 等多个区域,导致大型页面难以维护。组合式函数(composables)可以轻松将“获取员工薪资”“计算实际到账”“格式化本地货币”封装成独立模块,在组件中按需调用。这种代码组织方式对多人协作的跨时区团队尤其友好,每个成员只需关注自己负责的那部分业务逻辑。
除了开发体验,Vue 3 的响应式系统也经过完全重写,基于 Proxy 实现了细粒度的依赖追踪,内存占用比 Vue 2 更低。HR 平台往往需要展示上千行员工数据,同时还要实时刷新审批状态,Vue 3 的初始化性能提升和更高效的渲染机制能显著改善大数据列表的交互流畅度。另外,Vue 3 对 TypeScript 的支持是内置级别的,全局 API、路由、状态管理都能获得完整的类型推断,这为长期演进的大型项目提供了扎实的静态保障。
// 组合式函数示例:useEmployeeSalary.ts
import { ref, computed } from 'vue'
export function useEmployeeSalary(employeeId: string) {
const baseSalary = ref<number>(0)
const currency = ref('USD')
const maybeAddBonus = (salary: number, bonus: number) => salary + bonus
async function fetchSalary() {
const res = await fetch(`/api/employees/${employeeId}/salary`)
const data = await res.json()
baseSalary.value = data.base
currency.value = data.currency
}
const annualSalary = computed(() => maybeAddBonus(baseSalary.value * 12, 5000))
return { baseSalary, currency, annualSalary, fetchSalary }
}
二、基于 Monorepo 的工程化基础设施
全球 HR 系统通常包含员工管理、薪资计算、考勤审批、合规报表等多个子应用。如果每个子应用都单独建仓,共享组件库和工具函数就很难同步升级。采用 Monorepo 结构,配合 pnpm workspace,可以将 packages 按业务边界拆分成独立模块,同时共享一条依赖链路。比如把 @hr/ui 作为基础组件库,把 @hr/utils 作为汇率、日期、税务计算工具包,主应用直接引用本地 package,发布和版本管理都变得透明可控。
工程化搭建的另一个关键是 Vite。Vite 基于原生 ESM 进行开发服务启动,不需要像 Webpack 那样打包全部依赖,冷启动速度往往在几百毫秒内。对于 HR 管理后台这种包含几十个路由模块的大项目,Vite 的按需编译优势非常明显。生产构建时,Vite 默认使用 Rollup 做依赖预打包和代码分割,配合 terser 或 esbuild 进行压缩,能够生成非常均衡的 chunk 体积。下面是典型的 Vite + Vue 3 配置片段,需要注意路径别名和代理设置。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@hr/ui': path.resolve(__dirname, 'packages/ui')
}
},
server: {
proxy: {
'/api': {
target: 'https://api.hr.ippipp.com',
changeOrigin: true
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
i18n: ['vue-i18n']
}
}
}
}
})
三、多语言与多币种的全球化设计
Papaya Global 支持遍布全球的薪资结算,前端需要同时处理界面翻译、数字格式、货币单位和时区差异。vue-i18n 的 v9 版本基于 Vue 3 组合式 API 设计,可以用 useI18n 在任意组件内获取 t、d、n 等函数。实际工程中,不应将翻译文件全部打包进主包,而是按语言和路由模块做懒加载,比如英语、中文、西班牙语分别生成独立 chunk,用户首次进入时只下载当前语言。这样首屏体积不会随支持语种数量线性增长。
货币格式不是简单的“金额加符号”,不同地区对千分位、小数位、负值写法都有差异。vue-i18n 底层依赖 Intl.NumberFormat,可以精确控制这些细节。下面代码展示了如何利用 composition API 中的 locale 和 datetime 处理,让日期和货币始终跟随当前区域设置。时区问题则需要单独处理,因为 HR 系统要求员工打卡时间必须钉在本地时区,此时推荐使用 dayjs 搭配自定义插件,而不是用 moment 的大型体积。
<template>
<section>
<h3>Salary Overview</h3>
<p>{{ t('salary.annual') }}: {{ currencySymbol }} {{ formatNumber(annualSalary) }}</p>
<p>{{ t('salary.payday') }}: {{ formatDate(payday) }}</p>
</section>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const { t, locale, d, n } = useI18n({ useScope: 'global' })
const payday = new Date('2025-12-20T10:00:00Z')
const annualSalary = 123456.78
const currencySymbol = computed(() => {
return new Intl.NumberFormat(locale.value, {
style: 'currency',
currency: 'EUR',
currencyDisplay: 'symbol'
}).formatToParts(0)[0]
})
</script>
四、权限模型与动态路由
HR 管理系统的权限体系十分复杂:超级管理员有全部数据访问权,区域 HR 只能查看本区域员工,部门负责人只能审批本部门的请假,审计员只能只读访问。直接在路由配置里通过 meta 字段写死角色列表,会导致权限调整时需要重新发布前端,而且嵌套权限很难表达。更好的做法是后端登录接口返回当前用户的权限码列表,前端用 Pinia store 保存这些权限码,并在初始化时通过 addRoute 方法动态注册可见路由。这样菜单和路由都由权限码驱动,一个角色对应一组能力集合,修改权限时不需要改动前端代码。
下面给出一个基于 Pinia 的动态路由实现思路。登录后先拉取用户信息,生成需要权限才能访问的路由表,再调用 router.addRoute 挂载到根路由下。同时利用路由守卫判断目标路由是否有权限码配置,从而控制页面跳转和菜单显隐。这种方式也便于从后端配置菜单名称和顺序,实现真正的权限配置中心化。需要注意的是,动态路由在刷新页面时可能丢失,所以要在应用初始化时重新获取用户信息并恢复路由表。
// stores/auth.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import router from '@/router'
interface AuthState {
permissions: string[]
dynamicRoutes: RouteRecordRaw[]
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
permissions: [],
dynamicRoutes: []
}),
actions: {
async loadPermissions() {
const res = await fetch('/api/me/permissions')
const data = await res.json()
this.permissions = data.permissions
this.generateDynamicRoutes()
},
generateDynamicRoutes() {
const routes = this.permissions.map(p => {
return {
path: `/module/${p}`,
name: p,
component: () => import(`@/views/modules/${p}.vue`),
meta: { permission: p }
}
})
routes.forEach(route => router.addRoute('layout', route))
this.dynamicRoutes = routes
}
}
})
五、性能优化与多环境打包
大型 HR 后台的体量很大,首屏必须做代码分割。Vue Router 的动态 import 语法天然支持按路由懒加载,同时结合 Vite 的 manualChunks 将 vue 核心框架、ui 库和业务代码拆分,可以充分利用浏览器缓存。对于那些权限码才对应的模块,更是可以做到“无权限不下载”。除了代码分割,函数组件的延迟渲染也很重要,像是员工列表这种长页面需要使用虚拟滚动,表格内部避免使用重型的响应式对象,这样能有效减少渲染开销。
跨区域部署时,不同国家的数据中心对静态资源有法规要求。工程化方案需要支持多环境配置,比如 .env.development、.env.staging、.env.production,以及针对特定地区的配置文件。构建脚本可以按环境注入 API 地址和 CDN 路径,使用 Vite 的 define 静态替换全局变量。Windows 服务器上的目录结构也需要注意,下面是一个路径示例说明反斜杠的用法:C:\CI\build\output 对应的 Nginx 静态目录。确保所有脚本路径使用相应的平台规范,以免在自动化部署时出现找不到文件的错误。
# .env.production.zh-cn VITE_API_BASE_URL=https://api.cn.hr-ippipp.com VITE_CDN_URL=https://cdn.hr-ippipp.com/assets/ VITE_BUILD_MODE=release
针对全球 CDN 部署,前端还需要考虑指纹标识和资源版本管理。比如构建后的文件名为 index-a1b2c3.js,升级时设置强缓存,回滚时仅切换版本号。同时建议在服务端根据 Accept-Language 头自动跳转到对应语言子域,比如 zh-cn.hr.ippipp.com 与 eu.hr.ippipp.com,这样可以在边缘节点缓存不同语言的静态资源。整个工程化体系完成后,HR 系统能够覆盖全球多区域,同时让前端代码库保持清晰的模块边界,真正实现一套代码、全球部署。