zouyu
6 天以前 aae0b49229d8798a2cc31a8449092e2db62e2407
绩效管理:人员考勤功能模块
已添加3个文件
已修改8个文件
1295 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/performance/attendance.js 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/resourceDemand/device/component/acquisitionConfig.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/rawMaterialInspection/index.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/unpass/components/OAProcess.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/performance/attendance/components/staffClockInRecord.vue 395 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/performance/attendance/index.vue 644 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/performance/class/index.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -44,6 +44,7 @@
    "big.js": "^6.2.2",
    "clipboard": "2.0.8",
    "core-js": "3.37.1",
    "dayjs": "^1.11.20",
    "dom-to-image": "^2.6.0",
    "echarts": "5.4.0",
    "element-resize-detector": "^1.2.4",
src/api/performance/attendance.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
// ç­æ¬¡ç›¸å…³æŽ¥å£
import request from "@/utils/request";
// ç»©æ•ˆç®¡ç†-人员考勤-查询人员打卡记录
export function getClockInRecord(query) {
  return request({
    url: "/staff/attendance/getClockInRecord",
    method: "get",
    params: query,
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-分页查询考勤记录
export function pageAttendanceRecord(query) {
  return request({
    url: "/staff/attendance/pageAttendanceRecord",
    method: "get",
    params: query,
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-校验选择的考勤时间是否存在同一人员的考勤记录
export function checkDutyDate(query) {
  return request({
    url: "/staff/attendance/checkDutyDate",
    method: "get",
    params: query,
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-保存或更新考勤记录
export function saveOrUpdateStaffAttendanceTrackingRecord(data) {
  return request({
    url: "/staff/attendance/saveOrUpdateStaffAttendanceTrackingRecord",
    method: "post",
    data: data,
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-删除考勤记录
export function removeStaffAttendanceTrackingRecord(ids) {
  return request({
    url: "/staff/attendance/removeStaffAttendanceTrackingRecord",
    method: "delete",
    data:ids
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-同步考勤记录
export function syncAttendanceRecord(query) {
  return request({
    url: "/staff/attendance/syncAttendanceRecord",
    method: "get",
    params: query
  });
}
// ç»©æ•ˆç®¡ç†-人员考勤-修改进出记录状态
export function changeEnableReport(data) {
  return request({
    url: "/staff/attendance/changeEnableReport",
    method: "post",
    data: data
  });
}
src/api/system/post.js
@@ -9,6 +9,14 @@
  })
}
// èŽ·å–å²—ä½é€‰æ‹©æ¡†åˆ—è¡¨
export function optionSelect() {
  return request({
    url: '/system/post/optionSelect',
    method: 'get'
  })
}
// æŸ¥è¯¢å²—位详细
export function getPost(postId) {
  return request({
src/api/system/user.js
@@ -195,3 +195,11 @@
    params: query,
  });
}
// èŽ·å–ç”¨æˆ·åˆ—è¡¨
export function selectAllUser() {
  return request({
    url: "/system/newUser/selectAllUser",
    method: "get",
  });
}
src/views/CNAS/resourceDemand/device/component/acquisitionConfig.vue
@@ -274,6 +274,8 @@
        { label: "txt", value: ".txt" },
        { label: "mysql", value: ".mysql" },
        { label: "mqtt", value: ".mqtt" },
        { label: "sqlserver", value: ".sqlserver" },
        { label: "serialPort", value: ".serialPort" },
        { label: "png", value: ".png" }
      ],
      spanList: [],
src/views/business/rawMaterialInspection/index.vue
@@ -5,8 +5,7 @@
        <el-row>
          <el-form-item label="IFS域" prop="contract">
            <el-select @keyup.enter.native="refreshTable" v-model="componentData.contract" clearable placeholder="请选择" size="small">
              <el-option label="ZTNS" value="ZTNS"/>
              <el-option label="KJNS" value="KJNS"/>
              <el-option v-for="(item,index) in contractList" :key="index" :label="item.label" :value="item.value"/>
            </el-select>
          </el-form-item>
          <el-form-item label="批号" prop="updateBatchNo">
@@ -119,12 +118,15 @@
      :visible.sync="declareDialogVisible" width="800px" @close="resetFormData">
      <el-form ref="declareObj" :inline="true" :model="declareObj" :rules="declareObjRules" label-width="130px"
        label-position="right">
        <el-form-item class="declareObj-form-item" label="IFS域:" prop="contract" style="width: calc(50% - 54px)">
          <el-tag :type="declareObj.contract==='ZTNS'?'':'success'">{{declareObj.contract}}</el-tag>
        </el-form-item>
        <el-form-item class="declareObj-form-item" label="订单号:" prop="orderNo">
          <el-input v-model="declareObj.orderNo" :disabled="declareType !== 'add'" class="addObj-info" clearable
            placeholder="" size="small"></el-input>
                    placeholder="" size="small"></el-input>
        </el-form-item>
        <el-form-item class="declareObj-form-item" label="IFS域:" prop="contract">
          <el-select v-model="declareObj.contract" v-if="declareType === 'add'" clearable size="small">
            <el-option v-for="(item,index) in contractList" :key="index" :label="item.label" :value="item.value"/>
          </el-select>
          <el-tag v-else :type="declareObj.contract==='ZTNS'?'':'success'">{{declareObj.contract}}</el-tag>
        </el-form-item>
        <el-form-item class="declareObj-form-item" label="零件号:" prop="partNo">
          <el-input v-model="declareObj.partNo" :disabled="declareType !== 'add'" class="addObj-info" clearable
@@ -747,6 +749,16 @@
      upLoading: false,
      orderTypeList: [],
      materialPropList: [],
      contractList:[
        {
          label:"ZTNS",
          value:"ZTNS"
        },
        {
          label:"KJNS",
          value:"KJNS"
        },
      ]
    }
  },
  mounted() {
@@ -1110,7 +1122,9 @@
              id: this.declareObj.id,
              updateBatchNo: this.declareObj.updateBatchNo,
              orderType: this.declareObj.orderType,
              materialProp: this.declareObj.materialProp
              materialProp: this.declareObj.materialProp,
              partNo: this.declareObj.partNo,
              contract: this.declareObj.contract
            }).then(res => {
              if (res.code === 200) {
                this.declareDialogVisible = false
@@ -1195,6 +1209,7 @@
        buyUnitMeas: '', // å•位
        isExpire: '', // å•位
        orderType: null, // é”€å”®è®¢å•分类
        materialProp: null, // ç‰©æ–™å±žæ€§
      }
    }
  },
src/views/business/unpass/components/OAProcess.vue
@@ -105,7 +105,7 @@
        },
        {
          id: 6,
          name: "6质量部",
          name: "6质量部处理意见",
          info: "",
          time: "",
          operator: "",
src/views/performance/attendance/components/staffClockInRecord.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,395 @@
<script>
import {getClockInRecord,changeEnableReport} from '@/api/performance/attendance'
export default {
  name: "staffClockInRecord",
  props: {
    queryParams: {
      type: Object,
      default: () => ({
        personCode:null,//人员编号
        swingDate:null,//刷卡时间
        shiftId:null//班次id
      }),
    },
  },
  data() {
    return {
      loading: false,
      tableData: [],
      //进出门类型列表
      enterOrExitList:[
        {
          label:'进门',
          value:1
        },
        {
          label:'出门',
          value:2
        },
        {
          label:'进/出门',
          value:3
        }
      ],
      //开门类型列表
      openTypeList:[
        {
          "value": 42,
          "label": "合法密码开门"
        },
        {
          "value": 43,
          "label": "非法密码开门"
        },
        {
          "value": 45,
          "label": "合法指纹开门"
        },
        {
          "value": 46,
          "label": "非法指纹开门"
        },
        {
          "value": 48,
          "label": "远程开门"
        },
        {
          "value": 49,
          "label": "按钮开门"
        },
        {
          "value": 50,
          "label": "钥匙开门"
        },
        {
          "value": 51,
          "label": "合法刷卡开门"
        },
        {
          "value": 52,
          "label": "非法刷卡开门"
        },
        {
          "value": 53,
          "label": "门磁事件"
        },
        {
          "value": 54,
          "label": "异常开门"
        },
        {
          "value": 55,
          "label": "异常关门"
        },
        {
          "value": 56,
          "label": "正常关门"
        },
        {
          "value": 57,
          "label": "正常开门"
        },
        {
          "value": 59,
          "label": "对讲请求事件"
        },
        {
          "value": 61,
          "label": "人脸合法开门"
        },
        {
          "value": 62,
          "label": "人脸非法开门"
        },
        {
          "value": 1421,
          "label": "RFID有源合法"
        },
        {
          "value": 1422,
          "label": "RFID无源合法"
        },
        {
          "value": 1423,
          "label": "RFID有源非法"
        },
        {
          "value": 1424,
          "label": "RFID无源非法"
        },
        {
          "value": 1433,
          "label": "黑名单事件"
        },
        {
          "value": 1436,
          "label": "人证合法开门"
        },
        {
          "value": 1437,
          "label": "人证非法开门"
        },
        {
          "value": 1438,
          "label": "人证和身份证非法开门"
        },
        {
          "value": 1439,
          "label": "人证和身份证合法开门"
        },
        {
          "value": 1448,
          "label": "RFID感应事件"
        },
        {
          "value": 1449,
          "label": "RFID非法感应事件"
        },
        {
          "value": 1450,
          "label": "RFID按键事件"
        },
        {
          "value": 1451,
          "label": "RFID非法按键事件"
        },
        {
          "value": 1455,
          "label": "先刷卡后密码合法开门"
        },
        {
          "value": 1456,
          "label": "先刷卡后密码非法开门"
        },
        {
          "value": 1461,
          "label": "刷卡+指纹组合合法开门"
        },
        {
          "value": 1462,
          "label": "刷卡+指纹组合非法开门"
        },
        {
          "value": 1463,
          "label": "多人合法开门"
        },
        {
          "value": 1464,
          "label": "多人非法开门"
        },
        {
          "value": 1467,
          "label": "人员编号+密码合法开门"
        },
        {
          "value": 1468,
          "label": "人员编号+密码非法开门"
        },
        {
          "value": 1469,
          "label": "人脸+密码合法开门"
        },
        {
          "value": 1470,
          "label": "人脸+密码非法开门"
        },
        {
          "value": 1471,
          "label": "指纹+密码合法开门"
        },
        {
          "value": 1472,
          "label": "指纹+密码非法开门"
        },
        {
          "value": 1473,
          "label": "指纹+人脸合法开门"
        },
        {
          "value": 1474,
          "label": "指纹+人脸非法开门"
        },
        {
          "value": 1475,
          "label": "刷卡+人脸合法开门"
        },
        {
          "value": 1476,
          "label": "刷卡+人脸非法开门"
        },
        {
          "value": 1487,
          "label": "指纹+人脸+密码合法开门"
        },
        {
          "value": 1488,
          "label": "指纹+人脸+密码非法开门"
        },
        {
          "value": 1489,
          "label": "刷卡+人脸+密码合法开门"
        },
        {
          "value": 1490,
          "label": "刷卡+人脸+密码非法开门"
        },
        {
          "value": 1491,
          "label": "刷卡+指纹+密码合法开门"
        },
        {
          "value": 1492,
          "label": "刷卡+指纹+密码非法开门"
        },
        {
          "value": 1493,
          "label": "卡+指纹+人脸组合合法开门"
        },
        {
          "value": 1494,
          "label": "卡+指纹+人脸组合非法开门"
        },
        {
          "value": 4603,
          "label": "卡+指纹+人脸+密码组合合法开门"
        },
        {
          "value": 4604,
          "label": "卡+指纹+人脸+密码组合非法开门"
        },
        {
          "value": 4626,
          "label": "人脸+安全帽合法开门"
        },
        {
          "value": 4627,
          "label": "人脸+安全帽非法开门"
        },
        {
          "value": 10001,
          "label": "健康码合法开门"
        },
        {
          "value": 10002,
          "label": "异常健康码开门"
        }
      ]
    };
  },
  watch: {
    queryParams: {
      handler(newVal, oldVal) {
        this.refreshTable();
      },
      deep: true,
    },
  },
  created() {
    this.refreshTable();
  },
  methods: {
    changeEnableReport(row){
      let data = {
        id:row.id,
        enableReport:row.enableReport
      }
      changeEnableReport(data).then(res=>{
        if(res.code===200){
          this.$message.success("操作成功")
          this.refreshTable()
        }
      })
    },
    formatterAttendanceType(value){
      let label = "";
      this.openTypeList.forEach(item=>{
        if(item.value===value){
          label = item.label
        }
      })
      return label;
    },
    formatterEnterOrExitType(value){
      let label = "";
      this.enterOrExitList.forEach(item=>{
        if(item.value===value){
          label = item.label
        }
      })
      return label;
    },
    getDataSourceTypeTag(type) {
      const tagMap = {
        0: 'success',
        1: '',
      };
      return tagMap[type] || '';
    },
    getDataSourceTypeText(type) {
      const textMap = {
        0: 'ICC同步',
        1: '手动新增',
      };
      return textMap[type] || '未知';
    },
    refreshTable() {
      this.loading = true;
      getClockInRecord({...this.queryParams}).then(res => {
        this.tableData = res.data;
        this.loading = false;
      }).catch(() => {
        this.loading = false;
      });
    },
  },
};
</script>
<template>
  <div class="clock-in-record">
    <div v-loading="loading">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="300"
        :header-cell-style="{textAlign:'center'}"
        :cell-style="{textAlign:'center'}"
      >
        <el-table-column type="index" label="序号" width="60"></el-table-column>
        <el-table-column label="是否纳入考勤" prop="enableReport" width="120">
          <template slot-scope="scope">
            <el-switch
              @change="changeEnableReport(scope.row)"
              v-model="scope.row.enableReport"
              active-color="#13ce66"
              inactive-color="#ff4949">
            </el-switch>
          </template>
        </el-table-column>
        <el-table-column prop="personCode" label="工号" min-width="150" width="150"></el-table-column>
        <el-table-column prop="personName" label="姓名" min-width="150" width="150"></el-table-column>
        <el-table-column prop="deptName" label="部门名称" min-width="150" width="150"></el-table-column>
        <el-table-column prop="cardNumber" label="卡号" min-width="150" width="150"></el-table-column>
        <el-table-column prop="swingTime" label="刷卡时间" min-width="180" width="180"></el-table-column>
        <el-table-column prop="channelName" label="考勤点通道名称" min-width="180" width="180"></el-table-column>
        <el-table-column prop="deviceName" label="考勤设备名称" min-width="150" width="150"></el-table-column>
        <el-table-column prop="deviceCode" label="考勤设备编码" min-width="150" width="150"></el-table-column>
        <el-table-column prop="enterOrExit" label="进出门类型" min-width="150" width="150" :formatter="(row)=>formatterEnterOrExitType(row.enterOrExit)"></el-table-column>
        <el-table-column prop="openType" label="开门类型" min-width="150" width="150" :formatter="(row)=>formatterAttendanceType(row.openType)"></el-table-column>
        <el-table-column prop="isSync" label="数据来源" min-width="150" width="150">
          <template slot-scope="scope">
            <el-tag :type="getDataSourceTypeTag(scope.row.isSync)">
              {{ getDataSourceTypeText(scope.row.isSync) }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<style scoped lang="scss">
</style>
src/views/performance/attendance/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,644 @@
<template>
  <div class="attendance-page">
    <div class="search">
      <div class="search_thing">
        <div class="search_input">
          <el-date-picker
            v-model="dateRange"
            type="datetimerange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :default-time="['00:00:00', '23:59:59']"
            value-format="yyyy-MM-dd HH:mm:ss"
            style="width: 340px"
            size="small"
            @change="refreshTable"
          ></el-date-picker>
        </div>
      </div>
      <div class="search_thing">
        <div class="search_input">
          <el-input
            v-model="queryParams.keyword"
            placeholder="工号或员工名称"
            size="small"
            style="width: 200px"
            clearable
            @keyup.enter.native="refreshTable"
          ></el-input>
        </div>
      </div>
      <div class="search_thing">
        <el-button size="mini" type="primary" @click="refreshTable()"
          >查 è¯¢</el-button
        >
        <el-button size="mini" @click="resetQuery">重置</el-button>
        <el-button size="mini" type="primary" @click="openAddAttendanceDialog()">手动新增</el-button>
        <el-button size="mini" type="success" @click="openSyncAttendanceDialog()">同步考勤记录</el-button>
      </div>
    </div>
    <div class="container">
      <div v-loading="loading">
        <el-table
          :data="tableData"
          border
          :height="tableHeight"
          style="width: 100%"
          :header-cell-style="{textAlign:'center'}"
          :cell-style="{textAlign:'center'}"
        >
          <el-table-column type="index" label="序号" width="60"></el-table-column>
          <el-table-column prop="personCode" label="工号" min-width="120" width="120"></el-table-column>
          <el-table-column prop="personName" label="姓名" min-width="120" width="120"></el-table-column>
          <el-table-column prop="deptName" label="部门名称" min-width="120" width="120"></el-table-column>
          <el-table-column prop="shiftId" label="班次类型" min-width="120" width="120">
            <template slot-scope="scope">
              <el-tag :type="getDictTypeByShift(scope.row.shiftId)">{{ getShiftByDic(scope.row.shiftId) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="result" label="考勤结果" min-width="120" width="120">
            <template slot-scope="scope">
              <el-tag v-if="getResultTag(scope.row.result)" :type="getResultTag(scope.row.result).type">{{getResultTag(scope.row.result).label}}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="swingDate" label="考勤时间" min-width="150" width="150"></el-table-column>
          <el-table-column label="签入/签出情况">
            <el-table-column prop="workDateTime" label="上班时间" min-width="160" width="160">
              <template slot-scope="scope">
                <el-tag type="success" v-if="scope.row.workDateTime && scope.row.workClockInState===1">{{ scope.row.workDateTime }}</el-tag>
                <el-tag type="danger" v-else-if="scope.row.workDateTime && scope.row.workClockInState===0">{{ scope.row.workDateTime }}</el-tag>
                <span v-else>{{ scope.row.workDateTime }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="offWorkDateTime" label="下班时间" min-width="160" width="160">
              <template slot-scope="scope">
                <el-tag type="success" v-if="scope.row.offWorkDateTime && scope.row.offClockInState===1">{{ scope.row.offWorkDateTime }}</el-tag>
                <el-tag type="danger" v-else-if="scope.row.offWorkDateTime && scope.row.offClockInState===0">{{ scope.row.offWorkDateTime }}</el-tag>
                <span v-else>{{ scope.row.offWorkDateTime }}</span>
              </template>
            </el-table-column>
          </el-table-column>
          <el-table-column label="考勤时长(h)">
            <el-table-column prop="plannedWorkHours" label="应勤时长" min-width="80" width="80"></el-table-column>
            <el-table-column prop="actualWorkHours" label="实际时长" min-width="80" width="80"></el-table-column>
            <el-table-column prop="absenceWorkHours" label="缺勤时长" min-width="80" width="80"></el-table-column>
          </el-table-column>
          <el-table-column prop="isSync" label="数据来源" min-width="120">
            <template slot-scope="scope">
              <el-tag v-if="scope.row.isSync===0" type="success">ICC同步</el-tag>
              <el-tag v-else-if="scope.row.isSync===1" type="info">手动新增</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="createUser" label="创建人" min-width="150" width="150" :formatter="(row)=>formatterUserName(row.createUser)"></el-table-column>
          <el-table-column prop="createTime" label="创建时间" min-width="180" width="180"></el-table-column>
          <el-table-column prop="updateUser" label="更新人" min-width="150" width="150" :formatter="(row)=>formatterUserName(row.updateUser)"></el-table-column>
          <el-table-column prop="updateTime" label="更新时间" min-width="180" width="180"></el-table-column>
          <el-table-column fixed="right" width="180" label="操作">
            <template slot-scope="scope">
              <el-button type="text" @click="openClockInDialog(scope.row)">进出记录</el-button>
              <el-button :disabled="scope.row.isSync===0" type="text" @click="openAddAttendanceDialog(scope.row)">编辑</el-button>
              <el-button :disabled="scope.row.isSync===0" type="text" class="remove-btn" @click="confirmRemoveRecord(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          :total="pagination.total"
          :page.sync="pagination.current"
          :limit.sync="pagination.size"
          @pagination="handlePageChange"
        />
      </div>
    </div>
    <el-dialog
      title="进出记录"
      :visible.sync="dialogVisible"
      width="60%">
      <staff-clock-in-record :key="Math.random()" :query-params="clockInQueryParams" ></staff-clock-in-record>
    </el-dialog>
    <el-dialog
      :title="attendanceForm.id?'编辑考勤记录':'新增考勤记录'"
      :visible.sync="addAttendanceVisible"
      @closed="closeAttendanceDialog"
      width="50%">
      <el-form ref="attendanceForm" style="margin-top:20px" :model="attendanceForm" :rules="attendanceRule" label-position="right" label-width="100px">
        <div>
          <el-divider content-position="left">基本信息</el-divider>
          <el-row >
            <el-col :span="12">
              <el-form-item label="工号" prop="personCode">
                <el-input size="small" v-model="attendanceForm.personCode" disabled></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="姓名" prop="personName">
                <el-select size="small" style="width:100%" v-model="attendanceForm.personName" placeholder="请选择员工姓名" @change="selectedPersonName" clearable>
                  <el-option v-for="(item,index) in userList" :key="index" :label="item.name" :value="item.name"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row >
            <el-col :span="12">
              <el-form-item label="部门名称" prop="deptName">
                <el-input size="small" v-model="attendanceForm.deptName" placeholder="请输入部门名称" clearable></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="考勤时间" prop="swingDate">
                <el-date-picker size="small" clearable style="width:100%" type="date" placeholder="请选择日期" value-format="yyyy-MM-dd" v-model="attendanceForm.swingDate" @change="checkDutyDate"></el-date-picker>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row >
            <el-col :span="12">
              <el-form-item label="班次类型" prop="shiftId">
                <el-select disabled size="small" style="width:100%" v-model="attendanceForm.shiftId" placeholder="请选择班次类型" clearable>
                  <el-option v-for="(item,index) in dailyTypeList" :key="index" :label="item.dictLabel" :value="item.dictValue"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
<!--              <el-form-item label="考勤结果" prop="result">-->
<!--                <el-select size="small" style="width:100%" clearable v-model="attendanceForm.result" placeholder="请选择考勤结果">-->
<!--                  <el-option v-for="(item,index) in resultList" :key="index" :label="item.label" :value="item.value"/>-->
<!--                </el-select>-->
<!--              </el-form-item>-->
            </el-col>
          </el-row>
          <el-divider content-position="left">签入/签出情况</el-divider>
          <el-row>
            <el-col :span="12">
              <el-form-item label="上班时间" prop="workTime">
                <el-time-picker
                  style="width:100%"
                  value-format="HH:mm"
                  v-model="attendanceForm.workDateTime"
                  format="HH:mm"
                  placeholder="上班时间">
                </el-time-picker>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="下班时间" prop="offWorkTime">
                <el-time-picker
                  style="width:100%"
                  value-format="HH:mm"
                  format="HH:mm"
                  v-model="attendanceForm.offWorkDateTime"
                  placeholder="下班时间">
                </el-time-picker>
              </el-form-item>
            </el-col>
          </el-row>
        </div>
      </el-form>
      <span slot="footer" class="dialog-footer">
          <el-button @click="addAttendanceVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="confirmAddAttendance()">ç¡® å®š</el-button>
      </span>
    </el-dialog>
    <el-dialog
      title="同步考勤记录"
      :visible.sync="syncAttendanceVisible"
      @close="()=>{syncDateRange=[]}"
      width="25%">
      <el-date-picker
        style="width:100%"
        v-model="syncDateRange"
        type="datetimerange"
        value-format="yyyy-MM-dd HH:mm:ss"
        :default-time="['00:00:00', '23:59:59']"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期">
      </el-date-picker>
      <span slot="footer" class="dialog-footer">
    <el-button @click="syncAttendanceVisible = false">取 æ¶ˆ</el-button>
    <el-button type="primary" @click="confirmSyncAttendance">ç¡® å®š</el-button>
  </span>
    </el-dialog>
  </div>
</template>
<script>
import StaffClockInRecord from "./components/staffClockInRecord.vue";
import {selectAllUser} from '@/api/system/user'
import {
  pageAttendanceRecord,
  checkDutyDate,
  saveOrUpdateStaffAttendanceTrackingRecord,
  removeStaffAttendanceTrackingRecord,
  syncAttendanceRecord
} from '@/api/performance/attendance'
import {getDicts} from "@/api/system/dict/data";
import dayjs from 'dayjs';
export default {
  name: "Attendance",
  components: {
    StaffClockInRecord
  },
  data() {
    return {
      syncDateRange:[],
      syncAttendanceVisible: false,
      attendanceForm:{
        workDataId: null,
        offWorkDataId: null,
        personCode: null,
        personName: null,
        deptName: "质量部",
        shiftId: null,
        swingDate: null,
        workDateTime: null,
        offWorkDateTime: null,
        result: null,
      },
      attendanceRule:{
        personName:{required:true,message:'请选择员工姓名',trigger:'change'},
        deptName:{required:true,message:'请输入部门名称',trigger:'blur'},
        dailyName:{required:true,message:'请输入班次名称',trigger:'blur'},
        shiftId:{required:true,message:'请选择班次类型',trigger:'change'},
        swingDate:{required:true,message:'请选择考勤时间',trigger:'change'},
        workDateTime:{required:true,message:'请选择上班时间',trigger:'change'},
        offWorkDateTime:{required:true,message:'请选择下班时间',trigger:'change'},
        result:{required:true,message:'请选择考勤结果',trigger:'change'},
      },
      addAttendanceVisible:false,
      dialogVisible:false,
      dateRange: [],
      queryParams: {
        startDate: "",
        endDate: "",
        keyword: "",
      },
      userList:[],
      loading: false,
      tableData: [],
      pagination: {
        current: 1,
        size: 20,
        total: 0,
      },
      tableHeight:0,
      resizeHandler: null, // é˜²æŠ–函数引用
      clockInQueryParams:{
        personCode:null,//人员编号
        swingDate:null,//考勤时间
        shiftId:null//班次id
      },
      dailyTypeList:[],
      resultList:[
        {
          label:"正常",
          value:1,
          type:"success"
        },
        {
          label:"异常",
          value:0,
          type:"danger"
        },
      ],
    };
  },
  created() {
    this.selectEnumByCategory()
    this.getUserList()
    this.getTableHeight();
    this.dateRange = this.getTimeRange()
    this.resizeHandler = this.debounce(() => {
      this.getTableHeight();
    }, 200);
    this.refreshTable();
  },
  mounted(){
    this.getTableHeight();
    window.addEventListener("resize",this.resizeHandler)
  },
  beforeDestroy(){
    window.removeEventListener("resize",this.resizeHandler)
  },
  methods: {
    getShiftByDic(e) {
      let obj = this.dailyTypeList.find((m) => m.dictValue == e);
      if (obj) {
        return obj.dictLabel;
      }
      return "无";
    },
    getDictTypeByShift(e) {
      let obj = this.dailyTypeList.find((m) => m.dictValue == e);
      if (obj) {
        return obj.listClass;
      }
      return "";
    },
    selectEnumByCategory() {
      getDicts("sys_class_type").then((response) => {
        this.dailyTypeList = response.data;
      });
    },
    /**
     * åˆå§‹åŒ–默认日期范围:近一个月(当前日期 - 30天 è‡³ å½“前日期)
     */
    getTimeRange(format = 'YYYY-MM-DD HH:mm:ss') {
      // èŽ·å–å½“å‰æ—¶é—´
      const now = dayjs();
      // èŽ·å–å½“å‰æ—¥æœŸçš„ã€Œæ—¥ã€ï¼ˆ1-31)
      const currentDate = now.date();
      let startTime, endTime;
      // æ ¸å¿ƒé€»è¾‘:判断当前日期是否大于25号
      if (currentDate > 25) {
        // âœ… æƒ…况1:当前日>25 â†’ å½“月26号 ~ æ¬¡æœˆ25号
        startTime = now.startOf('month').add(25, 'day'); // å½“月1号 +25天 = 26号
        endTime = startTime.add(1, 'month').date(25).hour(23)
          .minute(59)
          .second(59);     // æ¬¡æœˆ25号(dayjs自动处理跨年)
      } else {
        // âœ… æƒ…况2:当前日≤25 â†’ ä¸Šæœˆ26号 ~ å½“月25号
        startTime = now.subtract(1, 'month').startOf('month').add(25, 'day'); // ä¸Šæœˆ26号
        endTime = now.date(25).hour(23)
          .minute(59)
          .second(59); // å½“月25号
      }
      // è¿”回格式化后的时间数组
      return [startTime.format(format), endTime.format(format)];
    },
    //同步考勤记录
    confirmSyncAttendance(){
      if(!this.syncDateRange || this.syncDateRange.length<2){
        this.$message.warning("请选择同步日期");
        return
      }
      syncAttendanceRecord({
        startDate:this.syncDateRange[0],
        endDate:this.syncDateRange[1]
      }).then(res=>{
        if(res.code===200){
          this.$message.success("后台同步考勤记录中,请刷新表格查看")
          this.refreshTable()
        }
      }).catch(error=>{
        console.error(error)
      })
      this.$nextTick(()=>{
        this.syncAttendanceVisible = false
      })
    },
    //打开同步考勤记录弹框
    openSyncAttendanceDialog(){
      this.syncAttendanceVisible = true
    },
    //删除考勤记录
    confirmRemoveRecord(row){
      if(!row){
        this.$message.warning("请选择一条记录")
        return
      }
      this.$confirm('是否确认删除该条考勤记录', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        let ids = [row.workDataId,row.offWorkDataId]
        removeStaffAttendanceTrackingRecord(ids).then(res=>{
          if(res.code===200){
            this.$message.success("删除成功")
            this.refreshTable()
          }
        }).catch(error=>{
          console.error(error)
        })
      }).catch(() => { });
    },
    //关闭新增/编辑考勤记录弹框
    closeAttendanceDialog(){
      this.attendanceForm.id = null
      this.$refs.attendanceForm.resetFields()
    },
    //重置考勤记录弹框表单
    resetAttendanceForm(){
      this.attendanceForm = {
        workDataId: null,
        offWorkDataId: null,
        personCode: null,
        personName: null,
        deptName: "质量部",
        shiftId: null,
        swingDate: null,
        workDateTime: null,
        offWorkDateTime: null,
        result: null,
      }
      this.$refs.attendanceForm.resetFields()
      this.$nextTick(()=>{
        this.addAttendanceVisible = false
      })
    },
    //确认保存考勤记录
    confirmAddAttendance(){
      this.$refs.attendanceForm.validate(valid=>{
        if(valid){
          saveOrUpdateStaffAttendanceTrackingRecord(this.attendanceForm).then(res=>{
            if(res.code===200){
              this.$message.success('保存成功')
              this.refreshTable()
              this.resetAttendanceForm()
            }
          }).catch(error=>{
            console.error(error)
          })
        }
      })
    },
    //校验所选考勤日期是否存在该人员的考勤记录
    checkDutyDate(val){
      if(!this.attendanceForm.personCode){
        this.$message.warning("请先选择人员名称")
        this.attendanceForm.dutyDate = null
        return
      }
      let params = {
        personCode:this.attendanceForm.personCode,
        swingDate: val
      }
      checkDutyDate(params).then(res=>{
        if(res.code===200&&res.data){
          this.attendanceForm.result = 1//默认正常
          this.attendanceForm.shiftId = res.data.shift
          this.attendanceForm.workDateTime = res.data.startTime
          this.attendanceForm.offWorkDateTime = res.data.endTime
        }
      }).catch(error=>{
        console.error(error)
        this.attendanceForm.swingDate = null
      })
    },
    //选择人员名称带出工号
    selectedPersonName(val){
      let account = ""
      this.userList.forEach(item=>{
        if(item.name===val){
          account = item.account
        }
      })
      this.attendanceForm.personCode = account
    },
    //打开新增/编辑考勤记录弹框
    openAddAttendanceDialog(row){
      if(row){
        //处理上/下班时间格式
        let workTime = row.workDateTime&&row.workDateTime.length>8?row.workDateTime.substring(11,16):row.workDateTime
        let offWorkTime = row.offWorkDateTime&&row.offWorkDateTime.length>8?row.offWorkDateTime.substring(11,16):row.offWorkDateTime
        row.workDateTime = workTime
        row.offWorkDateTime = offWorkTime
        this.attendanceForm = {...row}
      }
      this.addAttendanceVisible = true
    },
    //打开查看打卡记录弹框
    openClockInDialog(row){
      if(row){
        this.clockInQueryParams.personCode = row.personCode
        this.clockInQueryParams.shiftId = row.shiftId
        this.clockInQueryParams.swingDate = row.swingDate
      }
      this.$nextTick(()=>{
        this.dialogVisible = true
      })
    },
    //格式化人员名称
    formatterUserName(value){
      let userName = ""
      this.userList.forEach(item=>{
        if(item.id===value){
          userName = item.name
        }
      })
      return userName;
    },
    //格式化考勤结果
    getResultTag(type) {
      let typeItem = null
      this.resultList.forEach(item=>{
        if(item.value===type){
          typeItem = item
        }
      })
      return typeItem;
    },
    //防抖函数
    debounce(fn, delay) {
      let timer = null;
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
      };
    },
    //动态获取表格高度
    getTableHeight(){
      const innerHeight = window.innerHeight
      const naviHeight = 96
      const otherHeight = 128
      this.tableHeight = innerHeight - naviHeight - otherHeight
    },
    //刷新表格
    refreshTable() {
      this.loading = true;
        if (this.dateRange && this.dateRange.length === 2) {
          this.queryParams.startDate = this.dateRange[0];
          this.queryParams.endDate = this.dateRange[1];
        } else {
          this.queryParams.startDate = "";
          this.queryParams.endDate = "";
        }
      pageAttendanceRecord({...this.queryParams, ...this.pagination}).then(res => {
        this.tableData = res.data.records;
        this.pagination.total = res.data.total;
        this.pagination.current = res.data.current;
        this.loading = false;
      }).catch(() => {
        this.loading = false;
      });
    },
    //分页切换
    handlePageChange(page) {
      this.pagination.current = page.page;
      this.pagination.size = page.limit;
      this.refreshTable();
    },
    //查询用户列表
    getUserList(){
      selectAllUser().then(res=>{
        this.userList = res.data
      }).catch(error=>{
        console.error(error)
      })
    },
    //重置按钮
    resetQuery() {
      this.dateRange = this.getTimeRange();
      this.queryParams = {
        startDate: "",
        endDate: "",
        keyword: "",
      };
      this.$nextTick(()=>{
        this.refreshTable()
      })
    },
  },
};
</script>
<style scoped lang="scss">
.attendance-page {
  padding: 10px;
}
.remove-btn.el-button--text{
  color:#ff4949
}
.search {
  height: 50px;
  display: flex;
  align-items: center;
  background-color: #fff;
  margin-left:16px;
}
.search_thing {
  display: flex;
  align-items: center;
  height: 50px;
  margin-right: 30px
}
.search_label {
  width: 90px;
  font-size: 14px;
  text-align: right;
}
.search_input {
  display: flex;
  align-items: center;
}
.container {
  background-color: #fff;
  padding: 0 16px;
  min-height: calc(100vh - 180px);
}
::v-deep .el-tabs__header {
  margin-bottom: 16px;
}
::v-deep .el-tabs__content {
  padding: 0;
}
</style>
src/views/performance/class/index.vue
@@ -265,10 +265,26 @@
          <span style="color: red; margin-right: 4px">*</span>人员名称:
        </div>
        <div class="search_input" style="width: calc(100% - 90px)">
          <el-select v-model="schedulingQuery.userId" placeholder="请选择" style="width: 100%" multiple clearable
            collapse-tags>
            <el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id">
            </el-option>
          <el-select v-model="schedulingQuery.userId" popper-class="select-with-all" placeholder="请选择" style="width: 100%" multiple collapse-tags clearable>
<!--            <el-option v-for="item in personList" :key="item.id" :label="item.name" :value="item.id">-->
<!--            </el-option>-->
            <template slot="prefix">
              <el-button
                type="text"
                size="mini"
                @click="handleSelectAll"
                style="margin: 4px 0;"
              >
                {{ isAllSelected ? '取消全选' : '全选' }}
              </el-button>
              <el-divider style="margin: 5px 0;" />
            </template>
            <el-option
              v-for="item in personList"
              :key="item.id"
              :label="item.name"
              :value="item.id"
            />
          </el-select>
        </div>
      </div>
@@ -378,7 +394,7 @@
      loading: false,
      schedulingQuery: {
        week: "",
        userId: null,
        userId: [],
        shift: "",
      },
      list: [],
@@ -399,6 +415,11 @@
      menuY: 0,
      selectedTarget: null,
    };
  },
  computed:{
    isAllSelected() {
      return this.schedulingQuery.userId.length === this.personList.length && this.personList.length > 0;
    },
  },
  watch: {
@@ -423,6 +444,14 @@
    document.removeEventListener('click', this.handleClickOutside)
  },
  methods: {
    handleSelectAll() {
      if (this.isAllSelected) {
        this.schedulingQuery.userId = [];
      } else {
        // åªé€‰ä¸­å¯ç”¨é€‰é¡¹çš„value
        this.schedulingQuery.userId = this.personList.map(item => item.id);
      }
    },
    handleContextMenu(target,e) {
      // é˜»æ­¢æµè§ˆå™¨é»˜è®¤å³é”®èœå•
      e.preventDefault()
@@ -526,40 +555,6 @@
        this.initYear();
      }
    },
    transFromNumber(num) {
      let changeNum = [
        "零",
        "一",
        "二",
        "三",
        "四",
        "五",
        "六",
        "七",
        "八",
        "九",
      ]; //changeNum[0] = "零"
      let unit = ["", "十", "百", "千", "万"];
      num = parseInt(num);
      let getWan = (temp) => {
        let strArr = temp.toString().split("").reverse();
        let newNum = "";
        for (var i = 0; i < strArr.length; i++) {
          newNum =
            (i == 0 && strArr[i] == 0
              ? ""
              : i > 0 && strArr[i] == 0 && strArr[i - 1] == 0
                ? ""
                : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) +
            newNum;
        }
        return newNum;
      };
      let overWan = Math.floor(num / 10000);
      let noWan = num % 10000;
      if (noWan.toString().length < 4) noWan = "0" + noWan;
      return overWan ? getWan(overWan) + "万" + getWan(noWan) : getWan(num);
    },
    init() {
      this.pageLoading = true;
      let year = this.query.year.getFullYear();
@@ -574,16 +569,6 @@
      }).then((res) => {
        this.pageLoading = false;
        this.list = res.data.page
        // this.list = res.data.page.map((item) => {
        //   for (let key in item.monthlyAttendance) {
        //     let type = this.getDayByDic(key);
        //     if (type != undefined || type != null) {
        //       item[`day${type}`] = item.monthlyAttendance[key];
        //     }
        //   }
        //   return item;
        // });
        console.log(this.list)
        let headerList = res.data.headerList;
        this.weeks = [];
        headerList.forEach((item) => {
@@ -649,7 +634,7 @@
          this.schedulingVisible = false;
          this.schedulingQuery = {
            week: "",
            userId: null,
            userId: [],
            shift: "",
          };
          this.refresh();
src/views/system/user/index.vue
@@ -114,48 +114,61 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="岗位">
              <el-select style="width:100%" v-model="form.postIds" multiple placeholder="请选择">
                <el-option
                  v-for="item in postOptions"
                  :key="item.postId"
                  :label="item.postName"
                  :value="item.postId"
                  :disabled="item.status == 1"
                ></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="角色" prop="roleIds">
              <el-select v-model="form.roleIds" multiple placeholder="请选择角色" clearable>
              <el-select style="width:100%" v-model="form.roleIds" multiple placeholder="请选择角色" clearable>
                <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId"
                  :disabled="item.status == 1"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="密码" prop="password">
              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="姓名EN" prop="nameEn">
              <el-input v-model="form.nameEn" placeholder="请输入姓名EN" maxlength="50" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="邮箱" prop="email">
              <el-input v-model="form.email" placeholder="请输入内容"></el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="单位" prop="company">
              <el-select v-model="form.company" placeholder="请选择单位" style="width: 100%" clearable>
                <el-option v-for="item in postOptions" :key="item.id" :label="item.company"
                <el-option v-for="item in companyOptions" :key="item.id" :label="item.company"
                  :value="item.id"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="归属部门" prop="deptId">
              <treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true"
                placeholder="请选择归属部门" />
                          placeholder="请选择归属部门" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="签名">
              <el-upload class="avatar-uploader" :action="uploadAction" :show-file-list="false"
@@ -166,11 +179,13 @@
              </el-upload>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="个人照片">
              <el-upload class="avatar-uploader" :action="uploadAction" :show-file-list="false"
                :headers="upload.headers" accept=".png, .jpg, .jpeg, .gif" :on-error="handleUploadError1"
                :on-success="handleUploadSuccess1" :before-upload="handleBeforeUpload1">
                         :headers="upload.headers" accept=".png, .jpg, .jpeg, .gif" :on-error="handleUploadError1"
                         :on-success="handleUploadSuccess1" :before-upload="handleBeforeUpload1">
                <img v-if="form.pictureUrl" :src="javaApi + '/img/' + form.pictureUrl" class="avatar" alt="">
                <i v-else class="el-icon-plus avatar-uploader-icon"></i>
              </el-upload>
@@ -278,8 +293,15 @@
  resetUserPwd,
  changeUserStatus,
  deptTreeSelect,
  selectCompaniesList, selectSimpleList, addPersonUser, uploadFile, selectRoleList, selectCustomEnum, addDepartment
  selectCompaniesList,
  selectSimpleList,
  addPersonUser,
  uploadFile,
  selectRoleList,
  selectCustomEnum,
  addDepartment
} from "@/api/system/user";
import {optionSelect} from '@/api/system/post'
import { getToken } from "@/utils/auth";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
@@ -323,6 +345,8 @@
      dateRange: [],
      // å²—位选项
      postOptions: [],
      //单位选项
      companyOptions:[],
      // è§’色选项
      roleOptions: [],
      // è¡¨å•参数
@@ -703,10 +727,11 @@
      this.reset();
      this.open = true;
      selectCustomEnum().then(res => {
        this.postOptions = res.data;
        this.companyOptions = res.data;
      })
      getUser().then(response => {
        this.roleOptions = response.roles;
        this.postOptions = response.posts
        this.title = "添加用户";
      });
    },
@@ -714,7 +739,7 @@
    handleUpdate(row) {
      this.reset();
      selectCustomEnum().then(res => {
        this.postOptions = res.data;
        this.companyOptions = res.data;
      })
      const userId = row.userId || this.ids;
      getUser(userId).then(response => {
@@ -722,6 +747,8 @@
        this.form.password = ''
        this.roleOptions = response.roles;
        this.$set(this.form, "roleIds", response.roleIds);
        this.postOptions = response.posts
        this.$set(this.form, "postIds", response.postIds);
        this.open = true;
        this.title = "修改用户";
      });