1. 小型企业薪资绩效系统的技术选型与架构设计
在当今数字化管理的大背景下,小型企业对于薪资绩效管理的需求日益增长。传统Excel表格已无法满足复杂计算、多维度评估和数据安全的需求。基于Node.js+Vue+ElementUI的技术栈组合,恰好能够提供轻量级但功能完备的解决方案。
这套技术组合的核心优势在于:
- 全JavaScript技术栈:前后端统一使用JavaScript语言,降低学习成本和开发门槛
- 渐进式框架特性:Vue的渐进式特性允许根据企业需求灵活扩展功能模块
- 组件化开发效率:ElementUI提供丰富的现成组件,加速管理界面开发
- 高性能异步处理:Node.js的非阻塞I/O特性非常适合薪资批量计算场景
典型的技术架构分为三个层次:
- 前端展示层:Vue2/3 + ElementUI + Vuex状态管理
- 后端服务层:Node.js(Express/Koa) + RESTful API
- 数据持久层:MySQL/MongoDB + TypeORM/Sequelize
提示:对于50人以下的小型企业,建议使用SQLite替代MySQL以简化部署,当员工规模超过100人时再考虑迁移到专业数据库。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建与项目初始化
2.1 Node.js环境配置
正确的环境配置是项目成功的基础。推荐使用nvm(Node Version Manager)管理多版本Node环境:
bash复制# Windows系统安装nvm-windows
choco install nvm
# 安装LTS版本的Node.js
nvm install 16.14.2
nvm use 16.14.2
# 验证安装
node -v
npm -v
常见问题解决方案:
- npm脚本执行策略错误:当出现"无法加载文件npm.ps1"错误时,以管理员身份运行PowerShell并执行:
powershell复制Set-ExecutionPolicy RemoteSigned -Scope CurrentUser - 镜像源配置:国内用户建议配置淘宝镜像加速
bash复制npm config set registry https://registry.npmmirror.com
2.2 Vue项目脚手架初始化
使用Vue CLI创建项目骨架:
bash复制npm install -g @vue/cli
vue create salary-system
cd salary-system
vue add element-ui
关键配置选项:
- 选择Manually select features
- 勾选Babel, Router, Vuex, CSS Pre-processors
- 选择Sass/SCSS作为CSS预处理器
- 选择ESLint + Standard config
2.3 后端服务初始化
创建Node.js服务目录并初始化:
bash复制mkdir server
cd server
npm init -y
npm install express body-parser cors mysql2 sequelize
基础Express服务代码示例(server/index.js):
javascript复制const express = require('express')
const bodyParser = require('body-parser')
const cors = require('cors')
const app = express()
app.use(bodyParser.json())
app.use(cors())
// 数据库连接配置
const sequelize = new Sequelize('salary_db', 'user', 'password', {
host: 'localhost',
dialect: 'mysql'
})
// 启动服务
app.listen(3000, () => {
console.log('Server running on port 3000')
})
3. 数据库设计与核心模型
3.1 数据库表结构设计
薪资绩效系统通常需要以下核心表:
-
员工表(employees)
sql复制CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, employee_no VARCHAR(20) UNIQUE, name VARCHAR(50) NOT NULL, department VARCHAR(50), position VARCHAR(50), base_salary DECIMAL(10,2), hire_date DATE, status TINYINT DEFAULT 1 ); -
绩效考核表(performance)
sql复制CREATE TABLE performance ( id INT PRIMARY KEY AUTO_INCREMENT, employee_id INT, month DATE, kpi_score DECIMAL(5,2), attendance_rate DECIMAL(5,2), bonus DECIMAL(10,2), FOREIGN KEY (employee_id) REFERENCES employees(id) ); -
薪资发放表(salary_records)
sql复制CREATE TABLE salary_records ( id INT PRIMARY KEY AUTO_INCREMENT, employee_id INT, pay_date DATE, base_salary DECIMAL(10,2), performance_bonus DECIMAL(10,2), tax DECIMAL(10,2), insurance DECIMAL(10,2), net_salary DECIMAL(10,2), FOREIGN KEY (employee_id) REFERENCES employees(id) );
3.2 Sequelize模型定义
使用ORM工具定义数据模型(server/models/Employee.js):
javascript复制const { DataTypes } = require('sequelize')
module.exports = (sequelize) => {
const Employee = sequelize.define('Employee', {
employeeNo: {
type: DataTypes.STRING(20),
unique: true,
field: 'employee_no'
},
name: {
type: DataTypes.STRING(50),
allowNull: false
},
department: DataTypes.STRING(50),
position: DataTypes.STRING(50),
baseSalary: {
type: DataTypes.DECIMAL(10,2),
field: 'base_salary'
},
hireDate: {
type: DataTypes.DATEONLY,
field: 'hire_date'
},
status: {
type: DataTypes.TINYINT,
defaultValue: 1
}
}, {
tableName: 'employees',
timestamps: false
})
return Employee
}
4. 前端界面开发与ElementUI集成
4.1 员工管理模块实现
使用ElementUI构建员工列表页面(src/views/employee/List.vue):
vue复制<template>
<div class="employee-container">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>员工列表</span>
<el-button
type="primary"
icon="el-icon-plus"
style="float: right"
@click="handleCreate">
新增员工
</el-button>
</div>
<el-table
:data="employeeList"
border
style="width: 100%"
v-loading="loading">
<el-table-column
prop="employeeNo"
label="工号"
width="120">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="120">
</el-table-column>
<el-table-column
prop="department"
label="部门">
</el-table-column>
<el-table-column
prop="position"
label="职位">
</el-table-column>
<el-table-column
prop="baseSalary"
label="基本工资"
width="120">
<template slot-scope="scope">
{{ scope.row.baseSalary | currency }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="180">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-card>
</div>
</template>
<script>
import { getEmployeeList } from '@/api/employee'
export default {
data() {
return {
employeeList: [],
currentPage: 1,
pageSize: 10,
total: 0,
loading: false
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
this.loading = true
getEmployeeList({
page: this.currentPage,
size: this.pageSize
}).then(response => {
this.employeeList = response.data.list
this.total = response.data.total
this.loading = false
})
},
handleCreate() {
this.$router.push('/employee/create')
},
handleEdit(index, row) {
this.$router.push(`/employee/edit/${row.id}`)
},
handleDelete(index, row) {
this.$confirm('确认删除该员工记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteEmployee(row.id).then(() => {
this.$message.success('删除成功')
this.fetchData()
})
})
},
handleSizeChange(val) {
this.pageSize = val
this.fetchData()
},
handleCurrentChange(val) {
this.currentPage = val
this.fetchData()
}
}
}
</script>
<style scoped>
.employee-container {
padding: 20px;
}
</style>
4.2 薪资计算组件开发
创建可复用的薪资计算器组件(src/components/SalaryCalculator.vue):
vue复制<template>
<el-card class="calculator-card">
<div slot="header">
<span>薪资计算器</span>
</div>
<el-form
:model="form"
label-width="120px"
ref="calculatorForm">
<el-form-item label="基本工资" prop="baseSalary">
<el-input-number
v-model="form.baseSalary"
:min="0"
:step="100"
controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="绩效系数" prop="kpiFactor">
<el-slider
v-model="form.kpiFactor"
:min="0.5"
:max="1.5"
:step="0.1"
show-input>
</el-slider>
</el-form-item>
<el-form-item label="出勤率(%)" prop="attendanceRate">
<el-input-number
v-model="form.attendanceRate"
:min="0"
:max="100"
:step="1"
controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="社保缴纳" prop="insurance">
<el-select v-model="form.insurance" placeholder="请选择">
<el-option
v-for="item in insuranceOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="calculate">计算</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
<el-divider></el-divider>
<div class="result-section" v-if="result.visible">
<h4>计算结果</h4>
<el-descriptions :column="1" border>
<el-descriptions-item label="应发工资">{{ result.grossSalary | currency }}</el-descriptions-item>
<el-descriptions-item label="社保扣除">{{ result.insuranceDeduction | currency }}</el-descriptions-item>
<el-descriptions-item label="个人所得税">{{ result.tax | currency }}</el-descriptions-item>
<el-descriptions-item label="实发工资" class="net-salary">{{ result.netSalary | currency }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</template>
<script>
export default {
data() {
return {
form: {
baseSalary: 5000,
kpiFactor: 1.0,
attendanceRate: 100,
insurance: 'standard'
},
insuranceOptions: [
{ value: 'none', label: '无社保' },
{ value: 'standard', label: '标准缴纳' },
{ value: 'full', label: '全额缴纳' }
],
result: {
visible: false,
grossSalary: 0,
insuranceDeduction: 0,
tax: 0,
netSalary: 0
}
}
},
methods: {
calculate() {
// 计算逻辑
const base = this.form.baseSalary
const kpiBonus = base * (this.form.kpiFactor - 1)
const attendanceSalary = base * (this.form.attendanceRate / 100)
this.result.grossSalary = base + kpiBonus + attendanceSalary
// 社保计算
switch(this.form.insurance) {
case 'none':
this.result.insuranceDeduction = 0
break
case 'standard':
this.result.insuranceDeduction = this.result.grossSalary * 0.105
break
case 'full':
this.result.insuranceDeduction = this.result.grossSalary * 0.225
break
}
// 个税计算(简化版)
const taxableIncome = this.result.grossSalary - this.result.insuranceDeduction - 5000
if (taxableIncome <= 0) {
this.result.tax = 0
} else if (taxableIncome <= 3000) {
this.result.tax = taxableIncome * 0.03
} else if (taxableIncome <= 12000) {
this.result.tax = taxableIncome * 0.1 - 210
} else {
this.result.tax = taxableIncome * 0.2 - 1410
}
this.result.netSalary = this.result.grossSalary - this.result.insuranceDeduction - this.result.tax
this.result.visible = true
},
resetForm() {
this.$refs.calculatorForm.resetFields()
this.result.visible = false
}
},
filters: {
currency(value) {
if (!value) return '¥0.00'
return '¥' + value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
}
}
</script>
<style scoped>
.calculator-card {
margin-bottom: 20px;
}
.result-section {
margin-top: 20px;
}
.net-salary {
font-weight: bold;
color: #f56c6c;
}
</style>
5. 后端API开发与业务逻辑
5.1 薪资计算核心算法
实现薪资计算的业务逻辑(server/services/salaryService.js):
javascript复制const Employee = require('../models/Employee')
const Performance = require('../models/Performance')
const SalaryRecord = require('../models/SalaryRecord')
class SalaryService {
/**
* 计算员工月薪
* @param {number} employeeId - 员工ID
* @param {string} month - 月份(YYYY-MM)
* @returns {Promise<Object>} 薪资计算结果
*/
async calculateMonthlySalary(employeeId, month) {
const employee = await Employee.findByPk(employeeId)
if (!employee) {
throw new Error('员工不存在')
}
const performance = await Performance.findOne({
where: {
employee_id: employeeId,
month: new Date(month + '-01')
}
})
const baseSalary = employee.baseSalary
const kpiBonus = performance ? baseSalary * (performance.kpiScore / 100 - 1) : 0
const attendanceDeduction = performance ? baseSalary * (1 - performance.attendanceRate / 100) : 0
const grossSalary = baseSalary + kpiBonus - attendanceDeduction
// 社保计算(简化版)
const insuranceRate = 0.105 // 社保费率10.5%
const insuranceDeduction = grossSalary * insuranceRate
// 个税计算(简化版)
const taxableIncome = grossSalary - insuranceDeduction - 5000 // 起征点5000
let tax = 0
if (taxableIncome > 0) {
if (taxableIncome <= 3000) {
tax = taxableIncome * 0.03
} else if (taxableIncome <= 12000) {
tax = taxableIncome * 0.1 - 210
} else {
tax = taxableIncome * 0.2 - 1410
}
}
const netSalary = grossSalary - insuranceDeduction - tax
return {
employeeId,
month,
baseSalary,
kpiBonus,
attendanceDeduction,
grossSalary,
insuranceDeduction,
tax,
netSalary
}
}
/**
* 批量计算部门薪资
* @param {string} department - 部门名称
* @param {string} month - 月份(YYYY-MM)
* @returns {Promise<Array>} 部门薪资列表
*/
async calculateDepartmentSalaries(department, month) {
const employees = await Employee.findAll({
where: { department }
})
const results = []
for (const employee of employees) {
try {
const salary = await this.calculateMonthlySalary(employee.id, month)
results.push(salary)
} catch (err) {
console.error(`计算员工${employee.name}薪资失败:`, err.message)
}
}
return results
}
}
module.exports = new SalaryService()
5.2 RESTful API设计
实现薪资相关的API端点(server/routes/salary.js):
javascript复制const express = require('express')
const router = express.Router()
const salaryService = require('../services/salaryService')
/**
* @api {get} /api/salary/employee/:id 获取员工薪资记录
* @apiName GetEmployeeSalaryRecords
* @apiGroup Salary
*
* @apiParam {Number} id 员工ID
*
* @apiSuccess {Object[]} records 薪资记录列表
*/
router.get('/employee/:id', async (req, res) => {
try {
const records = await SalaryRecord.findAll({
where: { employee_id: req.params.id },
order: [['pay_date', 'DESC']]
})
res.json(records)
} catch (err) {
res.status(500).json({ error: err.message })
}
})
/**
* @api {post} /api/salary/calculate 计算薪资
* @apiName CalculateSalary
* @apiGroup Salary
*
* @apiParam {Number} employeeId 员工ID
* @apiParam {String} month 月份(YYYY-MM)
*
* @apiSuccess {Object} salary 薪资计算结果
*/
router.post('/calculate', async (req, res) => {
try {
const { employeeId, month } = req.body
const salary = await salaryService.calculateMonthlySalary(employeeId, month)
res.json(salary)
} catch (err) {
res.status(400).json({ error: err.message })
}
})
/**
* @api {post} /api/salary/batch 批量计算部门薪资
* @apiName BatchCalculateSalaries
* @apiGroup Salary
*
* @apiParam {String} department 部门名称
* @apiParam {String} month 月份(YYYY-MM)
*
* @apiSuccess {Object[]} salaries 部门薪资列表
*/
router.post('/batch', async (req, res) => {
try {
const { department, month } = req.body
const salaries = await salaryService.calculateDepartmentSalaries(department, month)
res.json(salaries)
} catch (err) {
res.status(400).json({ error: err.message })
}
})
module.exports = router
6. 系统部署与性能优化
6.1 生产环境部署方案
推荐使用PM2管理Node.js进程:
bash复制# 全局安装PM2
npm install -g pm2
# 启动服务
pm2 start server/index.js --name salary-api
# 设置开机自启
pm2 startup
pm2 save
前端项目构建与部署:
bash复制# 构建生产环境代码
npm run build
# 将dist目录部署到Nginx
sudo cp -r dist/* /var/www/html/salary-system/
Nginx配置示例(/etc/nginx/conf.d/salary.conf):
nginx复制server {
listen 80;
server_name salary.yourdomain.com;
root /var/www/html/salary-system;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
6.2 性能优化技巧
-
数据库查询优化:
- 为常用查询字段添加索引
- 使用Sequelize的include进行关联查询,避免N+1问题
- 批量操作时使用事务保证数据一致性
-
API响应优化:
- 添加缓存中间件(如redis)
- 实现分页查询避免大数据量传输
- 使用compression中间件压缩响应
-
前端性能优化:
- 路由懒加载
- 第三方库按需引入
- 使用keep-alive缓存常用组件
javascript复制// 缓存中间件示例
const apicache = require('apicache')
const cache = apicache.middleware
// 缓存薪资查询结果1小时
router.get('/employee/:id', cache('1 hour'), async (req, res) => {
// ...
})
7. 安全防护与权限控制
7.1 JWT认证实现
安装依赖:
bash复制npm install jsonwebtoken bcryptjs
实现用户认证服务(server/services/authService.js):
javascript复制const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const User = require('../models/User')
const SECRET_KEY = 'your-secret-key-here'
const TOKEN_EXPIRES = '8h'
class AuthService {
/**
* 用户登录
* @param {string} username 用户名
* @param {string} password 密码
* @returns {Promise<string>} JWT令牌
*/
async login(username, password) {
const user = await User.findOne({ where: { username } })
if (!user) {
throw new Error('用户不存在')
}
const isValid = await bcrypt.compare(password, user.password)
if (!isValid) {
throw new Error('密码错误')
}
return jwt.sign(
{ id: user.id, role: user.role },
SECRET_KEY,
{ expiresIn: TOKEN_EXPIRES }
)
}
/**
* 验证JWT令牌
* @param {string} token JWT令牌
* @returns {Promise<Object>} 解码后的令牌内容
*/
async verifyToken(token) {
try {
return jwt.verify(token, SECRET_KEY)
} catch (err) {
throw new Error('无效的令牌')
}
}
}
module.exports = new AuthService()
7.2 路由权限控制
创建权限中间件(server/middlewares/auth.js):
javascript复制const authService = require('../services/authService')
/**
* JWT认证中间件
*/
function authenticate(req, res, next) {
const token = req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: '请提供认证令牌' })
}
try {
const decoded = authService.verifyToken(token)
req.user = decoded
next()
} catch (err) {
res.status(401).json({ error: err.message })
}
}
/**
* 角色检查中间件
* @param {string} role 所需角色
*/
function authorize(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: '无权访问此资源' })
}
next()
}
}
module.exports = { authenticate, authorize }
在路由中使用权限控制:
javascript复制const { authenticate, authorize } = require('../middlewares/auth')
// 需要认证的路由
router.get('/employee/:id', authenticate, async (req, res) => {
// ...
})
// 需要管理员角色的路由
router.post('/salary/adjust', authenticate, authorize('admin'), async (req, res) => {
// ...
})
7.3 前端路由守卫
实现Vue路由权限控制(src/router/index.js):
javascript复制import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '../store'
import { getToken } from '@/utils/auth'
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { public: true }
},
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{
path: 'salary',
name: 'Salary',
component: () => import('@/views/salary/List.vue'),
meta: { requiresAuth: true }
},
{
path: 'employee',
name: 'Employee',
component: () => import('@/views/employee/List.vue'),
meta: { requiresAuth: true, roles: ['admin', 'hr'] }
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
router.beforeEach((to, from, next) => {
const isAuthenticated = getToken()
// 如果路由需要认证且用户未登录,重定向到登录页
if (to.matched.some(record => record.meta.requiresAuth) && !isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// 如果用户已登录但尝试访问登录页,重定向到首页
if (to.name === 'Login' && isAuthenticated) {
next({ name: 'Dashboard' })
return
}
// 检查角色权限
if (to.matched.some(record => record.meta.roles)) {
const userRole = store.getters.role
const hasPermission = to.matched.some(record =>
record.meta.roles.includes(userRole)
)
if (!hasPermission) {
next({ name: 'Dashboard' })
return
}
}
next()
})
export default router
8. 系统扩展与进阶功能
8.1 薪资条邮件通知
使用nodemailer实现薪资条邮件发送功能:
bash复制npm install nodemailer
邮件服务实现(server/services/emailService.js):
javascript复制const nodemailer = require('nodemailer')
const fs = require('fs')
const path = require('path')
const handlebars = require('handlebars')
// 创建邮件传输器
const transporter = nodemailer.createTransport({
host: 'smtp.your-email-provider.com',
port: 587,
secure: false,
auth: {
user: 'your-email@example.com',
pass: 'your-email-password'
}
})
// 编译邮件模板
const templatePath = path.join(__dirname, '../templates/salaryEmail.hbs')
const templateSource = fs.readFileSync(templatePath, 'utf8')
const template = handlebars.compile(templateSource)
class EmailService {
/**
* 发送薪资条邮件
* @param {Object} employee 员工信息
* @param {Object} salary 薪资信息
* @returns {Promise}
*/
async sendSalaryEmail(employee, salary) {
const mailOptions = {
from: '"HR系统" <hr@yourcompany.com>',
to: employee.email,
subject: `${salary.month}月份薪资条`,
html: template({
name: employee.name,
month: salary.month,
baseSalary: salary.baseSalary,
kpiBonus: salary.kpiBonus,
attendanceDeduction: salary.attendanceDeduction,
insuranceDeduction: salary.insuranceDeduction,
tax: salary.tax,
netSalary: salary.netSalary
})
}
return transporter.sendMail(mailOptions)
}
}
module.exports = new EmailService()
邮件模板示例(server/templates/salaryEmail.hbs):
html复制<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{month}}月份薪资条</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { text-align: center; margin-bottom: 30px; }
.salary-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
.salary-table th, .salary-table td { border: 1px solid #ddd; padding: 8px; text-align: left; }
.salary-table th { background-color: #f2f2f2; }
.net-salary { font-weight: bold; color: #f56c6c; }
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>{{month}}月份薪资条</h2>
<p>尊敬的{{name}},您好!</p>
</div>
<table class="salary-table">
<tr>
<th>项目</th>
<th>金额(元)</th>
</tr>
<tr>
<td>基本工资</td>
<td>{{baseSalary}}</td>
</tr>
<tr>
<td>绩效奖金</td>
<td>{{kpiBonus}}</td>
</tr>
<tr>
<td>考勤扣款</td>
<td>{{attendanceDeduction}}</td>
</tr>
<tr>
<td>社保公积金</td>
<td>{{insuranceDeduction}}</td>
</tr>
<tr>
<td>个人所得税</td>
<td>{{tax}}</td>
</tr>
<tr class="net-salary">
<td>实发工资</td>
<td>{{netSalary}}</td>
</tr>
</table>
<div class="footer">
<p>此邮件由系统自动发送,请勿直接回复</p>
<p>如有疑问,请联系人力资源部</p>
</div>
</div>
</body>
</html>
8.2 数据导出与报表生成
使用exceljs实现Excel报表导出:
bash复制npm install exceljs
Excel导出服务(server/services/excelService.js):
javascript复制const ExcelJS = require('exceljs')
const fs = require('fs')
const path = require('path')
class ExcelService {
/**
* 导出部门薪资报表
* @param {Array} salaries 薪资数据
* @param {string} department 部门名称
* @param {string} month 月份
* @returns {Promise<string>} 文件路径
*/
async exportDepartmentSalary(salaries, department, month) {
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('部门薪资')
// 设置列定义
worksheet.columns = [
{ header: '员工编号', key: 'employeeNo', width: 15 },
{ header: '姓名', key: 'name', width: 10 },
{ header: '基本工资', key: 'baseSalary', width: 12, style: { numFmt: '"¥"#,##0.00' } },
{ header: '绩效奖金', key: 'kpiBonus', width: 12, style: { numFmt: '"¥"#,##0.00' } },
{ header: '考勤扣款', key: 'attendanceDeduction', width: 12, style: { numFmt: '"¥"#,##0.00' } },
{ header: '社保公积金', key: 'insuranceDeduction', width: 12, style: { numFmt: '"¥"#,##0.00' } },
{ header: '个人所得税', key: 'tax', width: 12, style: { numFmt: '"¥"#,##0.00' } },
{ header: '实发工资', key: 'netSalary', width: 12, style: { numFmt: '"¥"#,##0.00' } }
]
// 添加数据行
salaries.forEach(salary => {
worksheet.addRow({
employeeNo: salary.employee.employeeNo,
name: salary.employee.name,
baseSalary: salary.baseSalary,
kpiBonus: salary.kpiBonus,
attendanceDeduction: salary.attendanceDeduction,
insuranceDeduction: salary.insuranceDeduction,
tax: salary.tax,
netSalary: salary.netSalary
})
})
// 添加汇总行
const totalRow = worksheet.addRow({})
totalRow.getCell('name').value = '合计'
totalRow.getCell('baseSalary').value = { formula: `SUM(C2:C${salaries.length + 1})` }
totalRow.getCell('kpiBonus').value = { formula: `SUM(D2:D${salaries.length + 1})` }
totalRow.getCell('attendanceDeduction').value = { formula: `SUM(E2:E${salaries.length + 1})` }
totalRow.getCell('insuranceDeduction').value = { formula: `SUM(F2:F${salaries.length + 1})` }
totalRow.getCell('tax').value = { formula: `SUM(G2:G${salaries.length + 1})` }
totalRow.getCell('netSalary').value = { formula: `SUM(H2:H${salaries.length + 1})` }
totalRow.font = { bold: true }
// 设置标题
worksheet.spliceRows(1, 0, [
[`${department} ${month}月份薪资报表`]
])
worksheet.mergeCells('A1:H1')
const titleCell = worksheet.getCell('A1')
titleCell.font = { size: 16, bold: true }
titleCell.alignment = { horizontal: 'center' }
// 确保导出目录存在
const exportDir =
