在 Vue 3 项目中落地一套类似 Gusto 的薪资与 HR 平台,并不只是把表单堆到页面上。这类系统涉及雇员入职、考勤、社保扣除、个税计算以及工资单分发,业务链路长且合规要求高。工程化的目标是在保证计算准确的前提下,让前端结构清晰、构建高效、权限可控。

工程化目录与构建层设计
很多团队一开始用 Vue CLI 或随意的 Vite 配置,把所有页面放在 views 目录里,结果薪资模块和 HR 模块互相引用,打包体积迅速膨胀。合理的做法是基于业务域做物理隔离:payroll、employee、admin 各自拥有 components、composables、store 子目录。Vite 的 manualChunks 可以把 Pinia 实例与薪资算法包单独拆出,避免每次改员工信息都重新构建薪酬核心。
在 vite.config.ts 中,我们可以通过函数式配置控制分包。下面示例将重量级的报表库与常规业务代码分离,同时开启 build.target 以兼容旧版内网浏览器:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
target: 'es2018',
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('payroll-engine')) {
return 'payroll-core';
}
if (id.includes('xlsx') || id.includes('pdf')) {
return 'report-lib';
}
return undefined;
}
}
}
}
});
这种分层不仅利于协作,也让 CI 流水线能针对 payroll-core 跑专项单元测试。当财务提出调整年终奖计税规则时,前端只需发布薪酬包而不必全量更新员工自助端,显著降低线上故障面。
状态管理与薪资计算的一致性
Gusto 类平台最怕的是同一员工的应发工资在 HR 界面和财务界面显示不一致。如果组件内各自用 ref 算一遍,节假日补班逻辑一变就全盘出错。我们用 Pinia 建立单一的 salaryStore,把所有税率表、社保基数、考勤扣减收归其中,组件只负责派发动作与展示快照。
下面的代码展示了如何在 store 中做安全的金额累加,使用整数分来规避浮点问题:
import { defineStore } from 'pinia';
export const useSalaryStore = defineStore('salary', {
state: () => ({
baseCents: 0,
overtimeCents: 0,
insuranceCents: 0
}),
getters: {
grossCents: (s) => s.baseCents + s.overtimeCents,
netCents: (s) => s.grossCents - s.insuranceCents
},
actions: {
setBase(monthlyYuan: number) {
this.baseCents = Math.round(monthlyYuan * 100);
},
addOvertime(hours: number, rateYuan: number) {
this.overtimeCents += Math.round(hours * rateYuan * 100);
}
}
});
通过将单位统一为分(cents),前端展示时再除以一百并格式化,能彻底绕开 0.1 + 0.2 !== 0.3 的坑。HR 在雇员档案页修改底薪,财务在工资单页看到的应发数会经由同一 getter 推导,从根源上保证跨角色数据一致,也方便后续接入审计日志。
多角色权限与路由解耦
薪资与 HR 系统通常存在员工、HR、财务、超级管理员四种角色。若在 vue-router 的导航守卫里写满 if (role === 'hr'),页面一多就变成纠缠的意大利面。更工程化的方式是把路由元信息声明为权限点,再用指令 v-permit 控制按钮级显隐,页面结构本身不感知具体账号。
如下示例定义了路由与全局守卫,结合后端返回的 permissions 数组做校验:
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/payroll/run',
component: () => import('../payroll/views/Run.vue'),
meta: { permit: 'payroll:execute' }
},
{
path: '/employee/profile',
component: () => import('../employee/views/Profile.vue'),
meta: { permit: 'self:read' }
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to, from, next) => {
const userPerms = window.__APP_USER__.permissions;
if (to.meta.permit && !userPerms.includes(to.meta.permit as string)) {
next('/403');
} else {
next();
}
});
这种解耦让产品能随时增删角色而不动业务组件。例如 Gusto 式的“仅员工本人看工资单、HR 看全员”规则,只需在后端调整 self:read 与 payroll:read 的分配,前端 v-permit="'payroll:read'"> 自动决定列表是否渲染。配合 Vue 3 的 <script setup> 语法,权限指令可封装为极轻量的 composable,进一步减少样板代码,使工程长期可维护。