劳动力管理软件的核心挑战在于把考勤、排班、工时、假勤等复杂规则稳定地落到代码里,而不是只做一套好看的界面。Kronos 这类商业系统之所以昂贵,很大程度在于规则引擎和时间计算的准确性。基于 Vue 3 从零搭建一个可扩展的劳动力管理系统,需要先把领域逻辑和 UI 剥离开,再通过 Pinia、TypeScript 与测试工具把规则沉淀下来。本文给出一个可落地的工程化方案,涵盖目录结构、状态管理、时间计算和组件拆分。

从领域模型入手,避免把业务写进组件
劳动力管理的难点是规则多且互相影响:一个夜班可能跨天,午休需要扣减,加班按不同倍率计算,假期额度还要随工龄变化。如果把这些逻辑直接写在 Vue 组件里,每增加一种班次规则,就可能要改动十几个文件。工程化的第一步是用 TypeScript 定义清晰的领域模型,让班次、打卡记录、请假申请和工时规则成为独立的类型。
可以把这些类型统一放在 src/domain 目录下,与 UI 层完全解耦。比如班次类型包含开始时间、结束时间、休息分钟数和是否跨天标记。考勤记录则记录员工、日期、打卡时间序列和状态。规则配置使用可序列化的对象,方便从后端接口获取或做本地覆盖。这样 Pinia 和组件只依赖这些稳定类型和纯函数,后续业务变化时修改成本会低很多。
export interface Shift {
id: string
name: string
startTime: string
endTime: string
breakMinutes: number
crossDay: boolean
}
export interface AttendanceRecord {
employeeId: string
date: string
clockIn: string[]
clockOut: string[]
status: 'normal' | 'late' | 'early' | 'missing'
}
export interface LaborRule {
overtimeMultiplier: Record<string, number>
holidayList: string[]
maxRegularHours: number
}
这里使用 Record<string, number> 而不是 any,可以避免加班倍率配置的键名随意化。领域模型中只保留业务概念,不包含任何 Vue 的响应式依赖,这样这些类型可以被 store、工具函数和测试代码安全复用。
用 Pinia 按聚合拆分状态,不建大而全的 store
很多工程一开始会创建一个 useAppStore 塞进所有数据,但劳动力管理涉及排班、考勤、规则和员工信息,单一 store 很快就会变得难维护。更合理的做法是按业务聚合拆分:排班 store 只负责班次模板和排班日历,考勤 store 只管理打卡记录和异常状态,规则 store 只缓存加班倍率、假期配置等。
使用 Pinia 的 setup 语法可以让状态声明更贴近组合式 API。例如排班 store 可以暴露 shifts、loading 和 fetchShifts 方法,内部用 ref 和 shallowRef 管理。shallowRef 适合放置不可变的列表数据,减少深层响应式代理的开销。异步请求失败时记录 error 信息,而不是只打印到控制台。
import { defineStore } from 'pinia'
import { ref, shallowRef } from 'vue'
export const useScheduleStore = defineStore('schedule', () => {
const shifts = shallowRef<Shift[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchShifts() {
loading.value = true
error.value = null
try {
const data = await api.getShifts()
shifts.value = data
} catch (e) {
error.value = e instanceof Error ? e.message : '加载排班失败'
} finally {
loading.value = false
}
}
return { shifts, loading, error, fetchShifts }
})
考勤 store 可以在 getter 中计算迟到次数和异常记录,但不要重复实现工时计算。工时和加班属于规则引擎,应当放到独立的工具模块。store 负责状态和异步,纯函数负责计算,这样测试时不需要挂载 Vue 实例。
把时间计算和规则引擎做成纯函数,用测试覆盖边界
工时计算是劳动力管理中最容易出错的环节之一。跨天班次、午休扣减、加班阶梯倍率和法定节假日都会影响最终结果。如果这些逻辑散落在组件或 store 里,一旦出现夏令时切换或特殊排班,排查成本极高。正确做法是把所有时间操作集中到 utils/time.ts 或 services/labor-engine.ts 中,形成无副作用的纯函数。
例如计算一个班次的实际时长,需要考虑结束时间小于开始时间的情况。可以把时间转换为分钟数再做差值,并减去休息时间。规则引擎则负责根据班次类型和节假日配置输出加班小时数。下面代码展示一个基础的跨天时长计算。
export function shiftDuration(shift: Shift): number {
const toMinutes = (time: string) => {
const [h, m] = time.split(':').map(Number)
return h * 60 + m
}
let minutes = toMinutes(shift.endTime) - toMinutes(shift.startTime)
if (minutes < 0) {
minutes += 24 * 60
}
return minutes - shift.breakMinutes
}
export function overtimeHours(regularMinutes: number, workedMinutes: number, multiplier: number): number {
const overtime = Math.max(0, workedMinutes - regularMinutes)
return overtime * multiplier
}
这些函数没有依赖 Vue 的响应式系统,可以直接用 Vitest 做单元测试。测试用例应覆盖 22:00 到次日 06:00 的班次、午休扣减、超时不足一小时、节假日倍率等场景。只有把边界锁定在测试里,后续重构时间库或引入 date-fns 时才能放心替换。
import { describe, it, expect } from 'vitest'
describe('shiftDuration', () => {
it('计算跨天班次时长', () => {
const shift = { id: '1', name: '夜班', startTime: '22:00', endTime: '06:00', breakMinutes: 60, crossDay: true }
expect(shiftDuration(shift)).toBe(7 * 60)
})
})
组件保持轻量,并把复杂校验交给领域服务
UI 层只负责渲染数据和收集用户输入,不应该承载工时规则或假勤判断。排班日历、考勤列表和请假申请表单可以拆成独立组件,通过 props 接收数据,通过 emit 回传事件。这样单个组件更容易做局部测试和样式调整。
在 Vue 单文件组件中,<script setup> 语法可以减少样板代码。组件内部用 store 获取数据,但计算逻辑调用工具函数。比如排班日历组件只关心如何展示班次块和时间轴,不关心夜班是否跨天,跨天判断交给 shiftDuration 或专门的 isCrossDay 函数。表单提交时先调用领域服务 validateLeaveRequest,通过后再调用 store 的提交方法。
<template>
<div class="schedule-calendar">
<div v-for="shift in shifts" :key="shift.id" class="shift-card">
<span>{{ shift.name }}</span>
<span>{{ shift.startTime }} - {{ shift.endTime }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useScheduleStore } from '@/stores/schedule'
const scheduleStore = useScheduleStore()
const shifts = computed(() => scheduleStore.shifts)
</script>
工程化的最后一步是统一路径别名、ESLint 规则和环境变量管理。在 Vite 中配置 alias 为 @ 指向 src,可以让导入路径更清晰。ESLint 开启 TypeScript 推荐规则和 Vue 3 插件,能减少低级错误。环境变量只用于服务地址和调试开关,不要把业务规则写进环境变量,规则应属于可版本化配置。
上述分层方式让 Vue 3 项目足以支撑劳动力管理软件的复杂需求。当业务扩展到多门店、多时区或更复杂的排班约束时,只需要在领域层增加类型和纯函数,在 store 中增加对应聚合,UI 组件基本不需要大改。Kronos 式系统的核心价值不在界面,而在于这些可测试、可演进的规则模块能否稳定运行。