Vue PC 后台与 uni-app 移动端如何协同完成考勤闭环?

很多考勤系统做不好,不是因为少了一个打卡按钮,而是因为 PC 后台、移动端、后端接口和统计报表各自为政。员工在手机上打卡,主管在流程里审批,人事在 PC 后台查记录,月底还要导出统计表。如果这些端对"迟到、缺卡、请假、补卡、外勤、申诉"的理解不一致,系统越上线越像一堆孤立页面。

我最近梳理智慧考勤项目时,第 9 篇最值得写的不是"PC 和移动端都有页面",而是它把多端职责拆得比较清楚:`zhkq-web` 负责规则和管理,`zhkq-uniapp` 负责现场采集和员工操作,`zhkq-api` 负责可信判断、流程回写和统计输出。这个分层对很多企业管理系统都能复用。

一、为什么考勤一定会变成多端协同问题?

考勤的输入来自现场,但规则不在现场。

员工打卡发生在手机上:定位、WiFi、蓝牙、拍照、外勤事由、请假申请、异常申诉,都更适合放在移动端。员工要的是"现在能不能打卡""为什么不能打卡""异常怎么提交"。

考勤规则维护发生在 PC 后台:考勤区域、考勤时间、WiFi、蓝牙、考勤规则、排班、组织、岗位、人员变动、日统计、轨迹查看,都更适合用表格、筛选、地图和批量操作来处理。

最终判断必须回到后端:移动端可以做提示,但不能成为最终裁判。用户手机时间可能不准,定位可能漂移,离线缓存可能延迟上传,人员规则可能在当天发生变更。后端必须重新基于规则、人员、时间和状态计算一次。

这个项目的目录正好对应三层职责:

复制代码
zhkq-web

  src/views/attendance/

  src/views/attendanceRecord/

  src/views/task/



zhkq-uniapp

  src/pages/dwdk/

  src/pages/wqdk/

  src/pages/qjgl/

  src/pages/abnormalAppealList/

  src/pages/cache-data/



zhkq-api

  jeecg-module-zhkq/src/main/java/org/jeecg/modules/biz/

    app/controller/

    manage/

    entity/

    enums/

    xxljob/

这不是简单的前后端分离,而是按业务角色拆端。

二、PC 后台:负责规则、记录、统计和权限边界

PC 后台不是给员工日常打卡用的,它更像考勤系统的"控制台"。在智慧考勤里,PC 端主要集中在这些目录:

复制代码
zhkq-web/src/views/attendance/area/

zhkq-web/src/views/attendance/wifi/

zhkq-web/src/views/attendance/bluetooth/

zhkq-web/src/views/attendance/time/

zhkq-web/src/views/attendance/rule/

zhkq-web/src/views/attendanceRecord/clockIn/

zhkq-web/src/views/attendanceRecord/daily/

zhkq-web/src/views/attendanceRecord/trackRecord/

zhkq-web/src/views/attendanceRecord/trackView/

这些页面背后对应的是"规则建模"和"结果查询"两类工作。

比如考勤记录列表接口在 PC 端是这样封装的:

复制代码
// zhkq-web/src/views/attendanceRecord/clockIn/clock.in.api.ts

enum Api {

  list = '/biz/kqAttendanceRecord/list',

  updateKqAttendanceRecord = '/biz/kqAttendanceRecord/edit',

  exportXls = '/biz/kqAttendanceRecord/exportXls',

  queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',

  handleDetail = '/biz/kqAttendanceRecord/queryById'

}



export const list = (params) => defHttp.get({ url: Api.list, params });

export const updateKqAttendanceRecord = (params) =>

  defHttp.put({ url: Api.updateKqAttendanceRecord, params });

这段代码不复杂,但它说明了 PC 后台的职责:按组织、人员、时间、状态筛选记录;必要时进行人工修正;最后支持导出。它不应该负责判断员工当前能不能打卡,也不应该负责采集定位。

每日统计也是同样思路:

复制代码
// zhkq-web/src/views/attendanceRecord/daily/daily.api.ts

enum Api {

  list = '/biz/kqAttendanceDayStats/list',

  exportXls = '/biz/kqAttendanceDayStats/exportXls',

  queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync'

}



export const list = (params) => defHttp.get({ url: Api.list, params });

export const getExportUrl = Api.exportXls;

后台关注的是"结果能不能解释、能不能筛选、能不能导出"。如果把这些功能塞进移动端,员工端会变重;如果 PC 后台只做展示,不处理权限和统计,又会让 HR 回到 Excel。

三、uni-app 移动端:负责现场动作和员工可理解的反馈

移动端的核心价值是采集现场事实。智慧考勤的 `zhkq-uniapp` 里,和考勤闭环相关的页面包括:

复制代码
src/pages/dwdk/index.vue

src/pages/wqdk/clockIn.vue

src/pages/qjgl/appeal.vue

src/pages/abnormalAppealList/index.vue

src/pages/abnormalAppealList/appeal-detail.vue

src/pages/officeTrajectory/index.nvue

src/pages/cache-data/index.vue

移动端必须比 PC 端更重视提示和容错。例如 `cache-data/index.vue` 就把离线场景单独做成页面,明确告诉员工当前有多少打卡缓存、多少轨迹记录、多少轨迹点位等待同步:

复制代码
<view class="title">考勤打卡缓存数据</view>

<view class="tips">

  您当前有

  <text class="num">{{locationStore.clockingInPoop.length}}</text>

  条考勤打卡数据待上传

</view>



<view class="title">轨迹定位缓存数据</view>

<view class="tips">

  您当前有

  <text class="num">

    {{locationStore.officeTrackPool.length + (locationStore.currentTrack ? 1 : 0)}}

  </text>

  条办公轨迹记录,以及

  <text class="num">{{locationStore.coordinatePool.length}}</text>

  个轨迹点位数据待上传

</view>

这个设计很实际。外勤场景下手机网络不稳定是常态,如果系统只提示"提交失败",员工会以为自己没打上卡;如果系统明确展示缓存数据,员工至少知道记录还在本机,后续可以同步。

请假页面也是移动端职责的典型例子。它不只是一个表单,而是要把员工能理解的字段收齐:

复制代码
<!-- zhkq-uniapp/src/pages/qjgl/appeal.vue -->

<picker :range="leaveTypeList" range-key="title" @change="confirmType">

  <view :class="!form.leaveType?'opcity':''">

    {{!form.leaveTypeName?'请选择请假类型':form.leaveTypeName}}

  </view>

</picker>



<textarea

  class="textarea-value"

  v-model="form.leaveArgument"

  placeholder="事由说明"

  maxlength="200" />



<u-upload

  :fileList="fileList"

  @afterRead="afterRead"

  @delete="deletePic"

  multiple

  :maxCount="4"

  :previewFullImage="true" />

这里的关键不是"有请假功能",而是移动端要让员工一次说清楚请假类型、开始结束日期、半天/全天、事由、图片材料。字段收不齐,后端流程就会变成反复退回;移动端体验不好,员工就会绕回聊天软件里说。

四、后端:统一接口语义,避免每端各算各的

多端协同最大的坑,是每端都有一套自己的状态理解。智慧考勤后端把常用状态收敛到枚举里,例如:

复制代码
// org.jeecg.modules.biz.enums.EClockStatus

public enum EClockStatus {

    NORMAL(0, "正常"),

    LATE(1, "迟到"),

    LACK(2, "缺卡"),

    LEAVE(3, "早退"),

    ABSENTEEISM(4, "旷工"),

    ASKFORLEAVE(5, "请假"),

    REISSUECARD(6, "补卡");

}

状态必须由后端统一维护。否则 PC 后台显示"补卡",移动端显示"正常",日统计又算成"缺卡",月底必然吵起来。

后端接口也体现了职责分离。移动端请假走 `/app/kqAfLeaveRecord`,后台记录查询走 `/biz/kqAttendanceRecord`,日统计查询走 `/biz/kqAttendanceDayStats`。例如 App 请假控制器:

复制代码
@RestController

@RequestMapping("/app/kqAfLeaveRecord")

public class AppKqAfLeaveRecordController {



    @PostMapping(value = "/add")

    public Result<String> add(@RequestBody @Valid AppKqAfLeaveRecordAddIn in) {

        kqAfLeaveRecordService.appAdd(BeanUtil.copyProperties(in, KqAfLeaveRecord.class));

        return Result.OK("提交成功");

    }



    @PostMapping(value = "/afCommit")

    public Result<String> afCommit(@RequestParam(name = "id", required = true) String id) {

        return kqAfLeaveRecordService.afCommit(id);

    }

}

PC 后台考勤记录查询则更关注权限过滤、条件筛选和分页:

复制代码
@GetMapping(value = "/list")

public Result<?> queryPageList(KqAttendanceRecordIn in,

                               @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,

                               @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {

    LambdaQueryWrapper<KqAttendanceRecord> queryWrapper = new LambdaQueryWrapper<>();

    if (StringUtils.isNotEmpty(in.getUnitId())) {

        queryWrapper.apply("(unit_id='" + in.getUnitId() + "' or department_id='" + in.getUnitId() + "')");

    } else {

        List<SysDepart> sysDeparts = iKqUserUnitService.queryDeparts(DataAuthUtil.getCurrentUserId());

        if (CollUtil.isNotEmpty(sysDeparts)) {

            List<String> collect = sysDeparts.stream().map(SysDepart::getId).collect(Collectors.toList());

            queryWrapper.in(KqAttendanceRecord::getDepartmentId, collect);

        }

    }

    queryWrapper.orderByDesc(KqAttendanceRecord::getClockTime);

    queryWrapper.select(KqAttendanceRecord::getId);

    IPage<KqAttendanceRecord> pageList = kqAttendanceRecordService.page(new Page<>(pageNo, pageSize), queryWrapper);

    return Result.OK(pageList);

}

这里有一个值得注意的工程点:查询先只 select `id`,分页后再 `listByIds` 获取详情。这样做通常是为了减少复杂字段分页时的负担,也方便后续扩展详情装配。

五、一次异常补卡如何跨端流转?

以"员工发现自己缺卡,提交异常申诉"为例,多端链路大致是:

复制代码
移动端异常列表

  -> 员工查看异常详情

  -> 提交异常申诉和图片

  -> 后端创建申诉记录并进入流程

  -> 主管或后台审批

  -> 后端回写考勤记录状态

  -> 日统计重新体现结果

  -> PC 后台可查询、导出、追溯

PC 后台编辑考勤记录时,如果状态变化,还会触发流程成功后的同步处理:

复制代码
KqAttendanceRecord db = kqAttendanceRecordService.getById(kqAttendanceRecord.getId());

kqAttendanceRecordService.updateById(kqAttendanceRecord);

if (db.getClockStatus() != kqAttendanceRecord.getClockStatus()) {

    QueryWrapper<KqAttendanceRecord> wrapper = new QueryWrapper<>();

    wrapper.eq("kq_attendance_record.id", kqAttendanceRecord.getId());

    List<KqCountDetailsVO> kqCountDetails = kqAttendanceRecordService.getKqCountDetails(wrapper);

    KqCountDetailsVO kqCountDetailsVO = kqCountDetails.get(0);

    afAbnormalAppealFlowService.handleSuccessful(kqCountDetailsVO, bizData, kqAttendanceRecord);

}

这段逻辑说明一个事实:考勤记录不是静态表。它会被请假、返岗、外勤、申诉、审批、补卡、定时任务共同影响。多端系统如果没有统一后端兜底,就很容易出现"页面改了,统计没变""审批通过了,报表还是异常"的问题。

六、日统计:让 PC 后台看到可解释结果

考勤闭环最终要落到统计。智慧考勤的日统计后台接口同样做了组织权限过滤、人员姓名过滤、日期过滤和状态过滤:

复制代码
if (StringUtils.isNotEmpty(in.getClockTime())) {

    queryWrapper.apply(

      "(date_format(clock_in_time1,'%Y-%m-%d')='" + in.getClockTime() + "' " +

      "or date_format(clock_in_time2,'%Y-%m-%d')='" + in.getClockTime() + "' " +

      "or date_format(clock_out_time1,'%Y-%m-%d')='" + in.getClockTime() + "' " +

      "or date_format(clock_out_time2,'%Y-%m-%d')='" + in.getClockTime() + "')");

}

if (StringUtils.isNotEmpty(in.getClockStatus())) {

    queryWrapper.apply(

      "(clock_in_type1='" + in.getClockStatus() + "' " +

      "or clock_in_type2='" + in.getClockStatus() + "' " +

      "or clock_out_type1='" + in.getClockStatus() + "' " +

      "or clock_out_type2='" + in.getClockStatus() + "')");

}

从技术角度看,这类查询后续还有优化空间,例如避免字符串拼接式 `apply`,改成参数化条件,减少 SQL 注入和索引失效风险。但从业务角度看,它表达了一个正确方向:日统计不只是汇总数字,还要能按具体日期、人员、状态和组织追溯回去。

七、多端协同的 6 个设计原则

我把这个项目里的经验总结成 6 条。

第一,移动端只做现场采集和用户反馈,不做最终裁判。它可以提示定位不在范围、当前不在考勤时间,但最终是否有效必须由后端确认。

第二,PC 后台负责规则和结果,不要让员工端承担复杂配置。考勤区域、时间、规则、排班、组织、统计、轨迹回放,都适合 PC 端处理。

第三,状态枚举必须统一。迟到、缺卡、早退、旷工、请假、补卡这类状态不能散落在多个前端常量里,后端应成为最终语义来源。

第四,流程要能回写记录。请假、返岗、异常申诉如果只保存在流程表里,不回写考勤记录和日统计,HR 月底还是要手工合并。

第五,离线缓存要被用户看见。移动端离线时,不只要缓存数据,还要让员工知道缓存了多少条、何时需要同步。

第六,后台查询必须带权限边界。考勤数据涉及人员位置、出勤状态和异常原因,不能让所有后台账号看全量数据。

八、可复用到其他企业系统的经验

考勤只是一个入口。类似的多端协同问题,在巡检、设备维保、门店拜访、项目现场、护理记录、车辆管理里都会出现。

如果你在设计企业系统,可以照这个模型拆:

复制代码
移动端:采集事实、提示异常、提交申请、查看进度

PC 后台:配置规则、审核流程、查询记录、导出报表

后端服务:统一状态、执行规则、写入记录、回写统计

定时任务:补偿缺失、重算历史、生成汇总

权限体系:按组织、部门、岗位过滤数据

好的多端系统不是把同一个页面复制到手机和 PC,而是让不同角色各做自己最适合做的事。员工端要轻,后台端要稳,后端要统一裁判,统计要可追溯。智慧考勤系统的价值也正在这里:它不是只记录谁点了打卡,而是把规则、现场、流程、审批和报表接成了一条能解释的业务链路。