企业级 HR 与合规平台的构建远非简单的增删改查,它涉及复杂的组织架构管理、薪酬流程、合规审计以及多角色权限体系。Vue 3 凭借组合式 API、性能提升和更好的 TypeScript 支持,成为构建此类复杂业务系统的理想选择。本文将从工程化视角出发,系统性地讨论如何利用 Vue 3 及其生态工具链搭建一个高可用、高安全性的 HR 与合规平台。

一、项目架构设计与模块化拆分策略
HR 平台的业务复杂度决定了项目不能采用简单的扁平结构。我们需要从物理目录划分、逻辑模块隔离和构建配置三个层面进行系统化设计。一个典型的 HR 平台至少包含员工管理、薪酬管理、考勤管理、合规审计、组织架构等核心模块,每个模块都有独立的业务逻辑和数据流。如果将这些模块的代码混在一起,随着业务增长,代码库会迅速变成难以维护的泥球架构。
在目录结构设计上,推荐采用基于功能的模块化拆分方式,而非基于文件类型的拆分。这意味着每个业务模块拥有自己独立的 components、views、store、api 和 types 目录,形成高内聚低耦合的模块边界。这种设计使得团队可以按模块进行并行开发,也方便后续将某个模块抽离为独立的微应用。同时,跨模块共享的资源统一放置在 shared 目录中,避免重复代码的产生。
src/ ├── modules/ │ ├── employee/ # 员工管理模块 │ │ ├── views/ │ │ ├── components/ │ │ ├── composables/ │ │ ├── api/ │ │ ├── store/ │ │ └── types/ │ ├── payroll/ # 薪酬管理模块 │ ├── attendance/ # 考勤管理模块 │ ├── compliance/ # 合规审计模块 │ └── organization/ # 组织架构模块 ├── shared/ # 跨模块共享资源 │ ├── components/ │ ├── composables/ │ ├── utils/ │ └── types/ ├── router/ ├── stores/ └── App.vue
在 Vite 构建配置层面,HR 平台通常需要处理大量的第三方依赖,如 PDF 生成库、日期处理库、图表库等,合理的代码分割和依赖优化能够显著提升首屏加载速度。通过 manualChunks 配置,可以将第三方依赖按类别拆分为独立的 chunk,避免单个 vendor 包过大。同时,对于合规审计等低频访问模块,可以配置路由级别的懒加载,实现按需加载,减少初始加载时间。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus', '@element-plus/icons-vue'],
'utils-vendor': ['lodash-es', 'dayjs', 'axios'],
'pdf-vendor': ['jspdf', 'jspdf-autotable'],
},
},
},
chunkSizeWarningLimit: 1000,
},
})此外,模块间通信是架构设计中不可忽视的问题。HR 平台中,员工模块的数据往往需要被薪酬模块、考勤模块等多个模块消费。直接跨模块引用 store 会导致耦合度急剧上升,一旦某个模块的 store 结构发生变化,所有依赖方都会受到影响。推荐的做法是定义清晰的模块接口契约,通过事件总线或共享的 composable 进行通信,确保模块间的依赖关系是单向且可追踪的。这种设计也为后续的微前端改造打下基础。
二、状态管理与多角色权限控制体系
HR 与合规平台的核心挑战之一是复杂的权限控制。一个企业中可能存在 HR 管理员、部门经理、普通员工、合规审计员、财务人员等多种角色,每种角色能访问的页面、能操作的数据范围都不同。Vue 3 生态中的 Pinia 提供了更轻量、更类型安全的状态管理方案,非常适合构建细粒度的权限体系。与 Vuex 相比,Pinia 去除了 mutation 的概念,API 更加直观,且天然支持 TypeScript 类型推导。
权限控制需要从路由级、页面级和按钮级三个层次进行设计。路由级权限通过动态路由注册实现,根据用户角色动态添加可访问的路由记录,未授权的路由根本不会注册到路由表中。页面级权限控制页面内不同区域的可见性,比如某些敏感操作面板仅对特定角色可见。按钮级权限则精确到单个操作按钮的显示与禁用,比如删除员工按钮只有超级管理员才能看到。三层权限体系层层递进,确保系统安全无死角。
// stores/permission.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
interface PermissionState {
roles: string[]
permissions: string[]
dynamicRoutes: RouteRecordRaw[]
}
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
roles: [],
permissions: [],
dynamicRoutes: [],
}),
actions: {
// 根据角色过滤路由
generateRoutes(roles: string[]): RouteRecordRaw[] {
this.roles = roles
const accessedRoutes = filterAsyncRoutes(allRoutes, roles)
this.dynamicRoutes = accessedRoutes
return accessedRoutes
},
// 检查按钮级权限
hasPermission(permission: string): boolean {
return this.permissions.includes(permission)
},
// 检查角色
hasRole(role: string): boolean {
return this.roles.includes(role)
},
},
})
// 路由过滤函数
function filterAsyncRoutes(routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] {
const res: RouteRecordRaw[] = []
routes.forEach((route) => {
const tmp = { ...route }
if (hasRoutePermission(tmp, roles)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
function hasRoutePermission(route: RouteRecordRaw, roles: string[]): boolean {
if (route.meta && route.meta.roles) {
return roles.some((role) => route.meta!.roles!.includes(role))
}
return true
}在按钮级权限控制方面,可以通过自定义指令实现声明式的权限校验。这种方式让权限控制逻辑与组件模板解耦,开发者只需在模板中添加指令即可完成权限校验,极大提升了开发效率。同时,自定义指令还可以在权限不通过时自动移除 DOM 元素,避免通过 CSS 隐藏带来的安全隐患,因为 CSS 隐藏的元素仍然可以通过浏览器开发者工具被操作。
// directives/permission.ts
import type { Directive, DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/stores/permission'
export const permission: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const permissionStore = usePermissionStore()
if (value && value instanceof Array && value.length > 0) {
const hasPermission = value.some((perm: string) =>
permissionStore.hasPermission(perm)
)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
} else {
throw new Error('需要指定权限标识')
}
},
}
// 在模板中使用示例
// <el-button v-permission="['employee:delete']">删除员工</el-button>对于数据级权限,即不同角色看到的数据范围不同,比如部门经理只能看到本部门员工数据,普通员工只能看到自己的信息,这部分逻辑建议在后端接口层面进行过滤,前端通过请求参数传递数据范围约束。前端不应承担数据过滤的安全责任,否则存在被绕过的风险。合规平台尤其需要确保数据访问的每一步都有审计日志记录,包括谁在什么时间访问了什么数据,这些日志需要持久化存储且不可篡改。
三、合规性数据流与敏感数据安全传输
HR 平台处理大量敏感个人信息,包括身份证号、薪资数据、银行账户、健康信息等,这些数据的传输和存储必须符合相关法律法规要求。从前端工程化角度,我们需要在数据请求、本地存储和页面展示三个环节建立完整的安全防护链路。任何一个环节的疏漏都可能导致严重的数据泄露事件,给企业带来法律风险和声誉损失。
在数据请求环节,所有 API 通信应强制使用 HTTPS,并对敏感字段进行前端加密。可以使用 AES 对称加密对请求体中的敏感数据进行加密,密钥通过非对称方式协商或由后端动态下发。同时,每个请求应携带时间戳和签名,防止重放攻击。Axios 拦截器是统一处理这些安全逻辑的理想位置,通过在拦截器中集中处理,避免在每个业务接口中重复编写安全代码。
// utils/request.ts
import axios from 'axios'
import CryptoJS from 'crypto-js'
// AES 加密函数
function encryptData(data: string, key: string): string {
return CryptoJS.AES.encrypt(data, key).toString()
}
// 生成请求签名
function generateSignature(params: Record<string, any>, timestamp: number): string {
const sortedKeys = Object.keys(params).sort()
const signStr = sortedKeys.map(k => `${k}=${params[k]}`).join('&')
return CryptoJS.HmacSHA256(signStr + timestamp, SECRET_KEY).toString()
}
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000,
})
// 请求拦截器:加密敏感数据
service.interceptors.request.use(
(config) => {
const timestamp = Date.now()
config.headers['X-Timestamp'] = timestamp.toString()
// 对敏感字段进行加密
if (config.data) {
const sensitiveFields = ['idCard', 'salary', 'bankAccount', 'healthInfo']
sensitiveFields.forEach((field) => {
if (config.data[field]) {
config.data[field] = encryptData(
config.data[field],
getSessionKey()
)
}
})
}
// 生成请求签名
config.headers['X-Signature'] = generateSignature(
config.params || config.data || {},
timestamp
)
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器:统一错误处理
service.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 处理 token 过期
redirectToLogin()
}
return Promise.reject(error)
}
)
export default service在本地存储方面,HR 平台应严格限制敏感数据的本地持久化。Pinia 的状态默认保存在内存中,不会持久化到 localStorage,这本身就是一个安全优势。如果确实需要缓存某些用户偏好设置,应避免存储任何包含个人身份信息的数据。对于必须本地缓存的配置类数据,可以使用加密存储方案,在写入 localStorage 前进行加密处理,读取时再解密。同时,应设置合理的过期时间,避免数据长期滞留在客户端。
在页面展示环节,敏感信息如身份证号、手机号等需要进行脱敏显示。可以通过自定义 composable 统一处理脱敏逻辑,确保全系统的脱敏规则一致。同时,对于薪酬明细等高敏感页面,应添加水印防截图功能,并记录页面访问日志。合规审计模块需要能够追溯每一次敏感数据的访问行为,包括访问者身份、访问时间、访问的数据范围以及操作类型等关键信息。
// composables/useDataMask.ts
import { computed } from 'vue'
export function useDataMask() {
// 身份证号脱敏:显示前3后4
const maskIdCard = (value: string): string => {
if (!value || value.length < 8) return value
return value.replace(/^(.{3}).*(.{4})$/, '$1************$2')
}
// 手机号脱敏:显示前3后4
const maskPhone = (value: string): string => {
if (!value || value.length !== 11) return value
return value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
// 银行卡号脱敏:显示后4位
const maskBankCard = (value: string): string => {
if (!value || value.length < 4) return value
return '**** **** **** ' + value.slice(-4)
}
// 薪资脱敏:显示区间
const maskSalary = (value: number): string => {
if (value < 5000) return '5K以下'
if (value < 10000) return '5K-10K'
if (value < 20000) return '10K-20K'
return '20K以上'
}
return {
maskIdCard,
maskPhone,
maskBankCard,
maskSalary,
}
}合规审计日志的记录不应仅依赖前端,但前端有责任在关键操作节点触发日志记录。可以在 Axios 响应拦截器中,对涉及敏感数据读取的接口响应自动触发审计日志上报。这种设计将审计逻辑与业务逻辑解耦,开发者无需在每个业务组件中手动添加日志代码,降低了遗漏的风险。审计日志应采用异步上报方式,避免影响主业务流程的响应速度,同时需要设计失败重试机制,确保日志不丢失。
最后,整个 HR 与合规平台的工程化还需要考虑国际化、主题切换、自动化测试等维度。国际化方面,Vue 3 的 vue-i18n 提供了完善的方案,HR 平台可能需要支持多语言环境下的薪酬格式、日期格式等本地化展示。自动化测试方面,建议对核心业务逻辑如薪酬计算、权限校验编写单元测试,对关键业务流程编写端到端测试,确保系统在迭代过程中保持稳定可靠。通过这些工程化手段的综合运用,才能构建出真正满足企业级需求的 HR 与合规平台。