Ver Fonte

增加日志功能

xjj há 1 ano atrás
pai
commit
faa5828d17

+ 4 - 0
config/router.config.js

@@ -60,6 +60,10 @@ export default [
             path: '/home/report/finance/project',
             component: './PurchaseAdmin/PurchaseList/Report/Finance/Project',
           },
+          {
+            path: '/home/report-daily',
+            component: './ReportDaily/Index',
+          },
         ],
       },
       {

+ 2 - 1
package.json

@@ -97,7 +97,8 @@
     "react-router-dom": "^4.3.1",
     "react-sizeme": "^2.6.7",
     "react-sortable-hoc": "^2.0.0",
-    "react-zmage": "^0.8.5"
+    "react-zmage": "^0.8.5",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@types/react": "^16.8.1",

+ 6 - 0
src/pages/PurchaseAdmin/PurchaseList/Index.js

@@ -103,6 +103,12 @@ function LayoutDetail(props) {
                 </SubMenu>
               )}
 
+              {currentUser.IsSuper && (
+                <Menu.Item key="/home/report-daily">
+                  <Link to="/home/report-daily">日志</Link>
+                </Menu.Item>
+              )}
+
               {/* <Menu.Item key="/home/demo" title="demo">
                 <Link to="/home/demo">demo</Link>
               </Menu.Item> */}

+ 98 - 0
src/pages/ReportDaily/Index.js

@@ -0,0 +1,98 @@
+import React, { useState } from 'react';
+import { DatePicker, Button, Calendar, Progress, Modal } from 'antd';
+import { getData } from './utils';
+import moment from 'moment';
+import ReportTable from './components/ReportTable';
+
+const ReportSummary = () => {
+  const [loading, setLoading] = useState(false);
+  const [selectedDate, setSelectedDate] = useState(moment());
+  const [progress, setProgress] = useState(0);
+  const [statusText, setStatusText] = useState('');
+  const [statusTextHistory, setStatusTextHistory] = useState([]);
+  const [historyVisible, setHistoryVisible] = useState(false);
+  const [data, setData] = useState(false);
+
+  const handleDateChange = date => {
+    setSelectedDate(date);
+  };
+
+  const handleQuery = async () => {
+    if (!selectedDate) {
+      return;
+    }
+
+    // 计算默认时间范围:上个月26号到这个月25号
+    const startDate = selectedDate
+      .clone()
+      .subtract(1, 'month')
+      .date(26)
+      .startOf('day');
+    const endDate = selectedDate
+      .clone()
+      .date(25)
+      .endOf('day');
+
+    setLoading(true);
+    try {
+      // 发起接口请求
+      let data = await getData(startDate, endDate, onChangeStatus);
+      setData(data);
+    } catch (error) {
+      console.error(error);
+      onChangeStatus('请求接口失败');
+    }
+    setLoading(false);
+  };
+
+  const onChangeStatus = (text, isDone) => {
+    let totalCalls = 300;
+    let newProgress = 0;
+    if (!isDone) {
+      let current = progress + 1 / totalCalls;
+      newProgress = current > 99 ? 99 : Number(current.toFixed(2));
+    } else {
+      newProgress = 100; // 直接到达100%
+    }
+    const currentTime = new Date().toLocaleTimeString();
+    let statusText = `【${currentTime}】:${text}`;
+    setProgress(newProgress);
+    setStatusText(statusText);
+    setStatusTextHistory(prevHistory => [statusText, ...prevHistory]);
+  };
+
+  const handleHistoryClick = () => {
+    setHistoryVisible(true);
+  };
+
+  const handleHistoryCancel = () => {
+    setHistoryVisible(false);
+  };
+
+  return (
+    <div>
+      <DatePicker
+        allowClear={false}
+        value={selectedDate}
+        onChange={handleDateChange}
+        picker="month"
+      />
+      <Button type="primary" loading={loading} onClick={handleQuery} style={{ marginLeft: 20 }}>
+        查询
+      </Button>
+      <div style={{ margin: '10px 0' }}>
+        {/* <Progress percent={50} status="active" /> */}
+        <div onClick={() => setHistoryVisible(true)}>{statusText}</div>
+      </div>
+      <ReportTable data={data} month={selectedDate.month() + 1} />
+
+      <Modal title="历史记录" visible={historyVisible} onCancel={handleHistoryCancel} footer={null}>
+        {statusTextHistory.map((item, index) => (
+          <div key={index}>{item}</div>
+        ))}
+      </Modal>
+    </div>
+  );
+};
+
+export default ReportSummary;

+ 125 - 0
src/pages/ReportDaily/components/ReportTable.js

@@ -0,0 +1,125 @@
+import React, { useMemo } from 'react';
+import { Button, Table, Tooltip } from 'antd';
+import * as XLSX from 'xlsx';
+
+const ReportTable = ({ data, month }) => {
+  // 将 data 对象解析为表格的 dataSource
+  const dataSource = useMemo(() => {
+    if (!data) return [];
+    return data.filter(
+      item => item.unsubmittedReports.length > 0 || item.lateSubmissions.length > 0
+    );
+  }, [data]);
+
+  const exportToExcel = () => {
+    const worksData1 = dataSource
+      .filter(item => item.unsubmittedReports.length > 0 || item.lateSubmissions.length > 0)
+      .map(item => ({
+        工号: item.userId,
+        姓名: item.name,
+        迟交次数: item.lateSubmissions.length,
+        漏交次数: item.unsubmittedReports.length,
+        请假次数: item.takingLeaveReports.length,
+      }));
+    const worksheet1 = XLSX.utils.json_to_sheet(worksData1);
+
+    const worksData2 = dataSource.map(item => ({
+      工号: item.userId,
+      姓名: item.name,
+      迟交: item.lateSubmissions.join(','),
+      漏交: item.unsubmittedReports.join(','),
+      请假: item.takingLeaveReports.join(','),
+      离职时间: item.resignationDate,
+      入职时间: item.hiredDate,
+    }));
+    const worksheet2 = XLSX.utils.json_to_sheet(worksData2);
+
+    const workbook = XLSX.utils.book_new();
+    XLSX.utils.book_append_sheet(workbook, worksheet1, '总览');
+    XLSX.utils.book_append_sheet(workbook, worksheet2, '详情');
+
+    const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
+    const data = new Blob([excelBuffer], {
+      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+    });
+    const url = URL.createObjectURL(data);
+    const link = document.createElement('a');
+    link.href = url;
+    link.setAttribute('download', `${month}月考勤数据.xlsx`);
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  };
+
+  // 表格列配置
+  const columns = [
+    {
+      title: '工号',
+      dataIndex: 'userId',
+      key: 'userId',
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '漏交',
+      dataIndex: 'unsubmittedReports',
+      key: 'unsubmittedReports',
+      render: unsubmittedReports => (
+        <Tooltip title={unsubmittedReports.join(',')}>
+          <a>{unsubmittedReports.length}</a>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '迟交',
+      dataIndex: 'lateSubmissions',
+      key: 'lateSubmissions',
+      render: lateSubmissions => (
+        <Tooltip title={lateSubmissions.join(',')}>
+          <a>{lateSubmissions.length}</a>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '请假',
+      dataIndex: 'takingLeaveReports',
+      key: 'takingLeaveReports',
+      render: takingLeaveReports => (
+        <Tooltip title={takingLeaveReports.join(',')}>
+          <a>{takingLeaveReports.length}</a>
+        </Tooltip>
+      ),
+    },
+    {
+      title: '离职日期',
+      dataIndex: 'resignationDate',
+      key: 'resignationDate',
+    },
+    {
+      title: '入职日期',
+      dataIndex: 'hiredDate',
+      key: 'hiredDate',
+    },
+    {
+      title: '操作',
+      render: () => <a></a>,
+    },
+  ];
+
+  return (
+    <Table
+      dataSource={dataSource}
+      footer={() => (
+        <Button onClick={exportToExcel} type="primary">
+          导出报表
+        </Button>
+      )}
+      columns={columns}
+    />
+  );
+};
+
+export default ReportTable;

+ 169 - 0
src/pages/ReportDaily/utils.js

@@ -0,0 +1,169 @@
+const path = require('path');
+const moment = require('moment');
+import {
+  getHoliday,
+  getUserTakingLeave,
+  getAllReport,
+  getHiredDate,
+  getResignationDate,
+} from '@/services/ReportDaily';
+
+async function getLateSubmissionsAndUnsubmittedReports(option) {
+  const { holiday, reportData, onProcess, startDate, endDate } = option;
+  const employeeSubmissions = {};
+
+  // 遍历报表数据,生成以工号为键、提交时间数组为值的数据结构
+  reportData.forEach(item => {
+    const employeeId = item.creator_id;
+    // if (employeeId != "16015142307766361") return;
+    const reportTime = moment(item.create_time, 'YYYY年MM月DD日 HH:mm');
+    if (!employeeSubmissions[employeeId]) {
+      employeeSubmissions[employeeId] = {
+        name: item.creator_name,
+        dates: [],
+      };
+    }
+    employeeSubmissions[employeeId].dates.push(reportTime);
+  });
+
+  // 遍历工号,检查每个工号对应的提交时间
+  for (const employeeId in employeeSubmissions) {
+    const submissions = employeeSubmissions[employeeId].dates;
+    console.log(`请求${employeeSubmissions[employeeId].name}的请假详情`);
+    onProcess?.(`请求${employeeSubmissions[employeeId].name}的请假详情`);
+    // 查询请假情况
+    const takingLeave = await getUserTakingLeave(startDate, endDate, employeeId);
+    // 判断是否为节假日或者请假
+    const isHoliday = date => {
+      let day = date.format('YYYY-MM-DD');
+      return holiday[day] || takingLeave[day];
+    };
+
+    // 根据日志提交时间、节假日、请假情况,获取未提交以及漏交记录
+    const { lateSubmissions, unsubmittedReports, takingLeaveReports } = await analyzeDates(
+      submissions,
+      startDate,
+      endDate,
+      holiday,
+      takingLeave
+    );
+    employeeSubmissions[employeeId].lateSubmissions = lateSubmissions;
+    employeeSubmissions[employeeId].unsubmittedReports = unsubmittedReports;
+    employeeSubmissions[employeeId].takingLeaveReports = takingLeaveReports;
+
+    delete employeeSubmissions[employeeId].dates;
+  }
+
+  onProcess('请求全部完成', true);
+
+  return Object.keys(employeeSubmissions).map(userId => ({
+    userId,
+    ...employeeSubmissions[userId],
+  }));
+}
+
+async function analyzeDates(dateArray, startDate, endDate, holiday, takingLeave) {
+  const lateSubmissions = [];
+  const unsubmittedReports = [];
+  const takingLeaveReports = [];
+  // 对时间进行排序
+  const sortedDates = dateArray.sort((a, b) => a - b);
+  const currentDay = startDate.clone();
+
+  while (currentDay.isSameOrBefore(endDate, 'day')) {
+    let index = -1;
+    // 一日可能有多条记录,通过遍历找到今日最后一次提交记录
+    sortedDates.forEach((time, i) => {
+      let flag = time.isSame(currentDay, 'day');
+      if (flag) index = i;
+    });
+    let date = index == -1 ? null : sortedDates[index];
+
+    // 无提交记录或者没有在9点以后提交都算未提交
+    if (!date || date.hour() < 9) {
+      let dayKey = currentDay.format('YYYY-MM-DD');
+      if (holiday[dayKey]) {
+        // 节假日不做处理
+      } else if (takingLeave[dayKey]) {
+        // 判断是否为请假
+        takingLeaveReports.push(dayKey);
+      } else {
+        // 今日未提交,根据次日提交情况判断是漏交还是迟交
+        let nextDay = sortedDates.find(time => time.diff(currentDay, 'day') == 1);
+        if (nextDay && nextDay.hour() < 9) {
+          lateSubmissions.push(dayKey);
+        } else {
+          unsubmittedReports.push(dayKey);
+        }
+      }
+    }
+
+    // 删除数组内多余数据,提高下次遍历效率
+    if (index != -1) sortedDates.splice(0, index);
+
+    currentDay.add(1, 'days');
+  }
+
+  return { lateSubmissions, unsubmittedReports, takingLeaveReports };
+}
+
+// 过滤入职以前的未提交记录
+async function filterByHireDate(employeeData, onProcess) {
+  // 获取未提交列表
+  let data = employeeData.filter(item => item.unsubmittedReports.length > 0);
+
+  for (let i = 0; i < data.length; i++) {
+    const item = data[i];
+    try {
+      const hiredDate = await getHiredDate(item.userId);
+      item.hiredDate = hiredDate;
+      item.unsubmittedReports = item.unsubmittedReports.filter(date =>
+        moment(date).isSameOrAfter(moment(hiredDate))
+      );
+      onProcess(`查询${item.name}入职时间成功`);
+    } catch (error) {
+      onProcess(`查询${item.name}入职时间失败`);
+      console.error(error);
+    }
+  }
+}
+
+// 过滤离职以后的未提交记录
+async function filterByResignationDate(employeeData) {
+  // 获取未提交列表
+  let data = employeeData.filter(item => item.unsubmittedReports.length > 0);
+  let userIds = data.map(item => item.userId);
+  // 根据未提交人的id查询离职情况
+  const dimissionInfos = await getResignationDate(userIds);
+  for (let i = 0; i < data.length; i++) {
+    const item = data[i];
+    let resignationDate = dimissionInfos[item.userId];
+    // 判断是否离职
+    if (!resignationDate) continue;
+    item.resignationDate = resignationDate;
+    // 根据离职时间过滤
+    item.unsubmittedReports = item.unsubmittedReports.filter(date =>
+      moment(date).isBefore(moment(resignationDate))
+    );
+  }
+}
+
+export async function getData(startTime, endTime, onProcess) {
+  let holiday = await getHoliday(onProcess);
+  let reportData = await getAllReport(startTime, endTime, onProcess);
+
+  // 根据节假日和时间段获取员工日志报表
+  let employeeData = await getLateSubmissionsAndUnsubmittedReports({
+    startDate: startTime,
+    endDate: endTime,
+    holiday,
+    reportData,
+    onProcess,
+  });
+
+  // 判断员工入职时间和离职时间
+  filterByHireDate(employeeData, onProcess);
+  filterByResignationDate(employeeData, onProcess);
+
+  return employeeData;
+}

+ 229 - 0
src/services/ReportDaily.js

@@ -0,0 +1,229 @@
+import axios from 'axios';
+const moment = require('moment');
+
+let api = axios.create({
+  baseURL: 'http://192.168.20.107:8010/api',
+});
+
+async function getToken() {
+  let url = '/token';
+  let res = await api.get(url, {
+    params: {
+      appkey: 'ding8xqenag7ilsxbw7n',
+      appsecret: 'VX4SWM7E8AzMEVBbUwCqfHVE8fgvCZHZPSUCx1WApY5Ne5xz162Ap0JEokVzti75',
+    },
+  });
+  return res.data.token;
+}
+
+let access_token = '';
+
+/**
+ * 根据用户和时间段查询请假情况
+ * @param {moment} startDate
+ * @param {moment} endDate
+ * @param {string} userid
+ * @returns
+ */
+export async function getUserTakingLeave(startDate, endDate, userid) {
+  if (!access_token) {
+    access_token = await getToken();
+  }
+
+  // 钉钉的接口 URL
+  const url = '/getcolumnval?access_token=' + access_token;
+
+  const requestData = {
+    column_id_list: '10914023', // [下班1打卡结果] 根据/attendance/getattcolumns查询得到
+    from_date: startDate.format('YYYY-MM-DD 00:00:00'),
+    to_date: endDate.format('YYYY-MM-DD 23:59:59'),
+    userid,
+  };
+
+  // 设置请求头
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: `Bearer ${access_token}`,
+  };
+  let catchKey = `getcolumnval|${userid}|${requestData.from_date}`;
+  let takingLeave = {};
+  let catchData = localStorage[catchKey];
+
+  // 判断是否缓存过数据
+  if (catchData) {
+    takingLeave = JSON.parse(catchData);
+  } else {
+    // 发送 POST 请求
+    const response = await api.post(url, requestData, { headers });
+
+    // 处理响应结果
+    const column_vals = response.data.result.column_vals[0].column_vals;
+    column_vals.forEach(item => {
+      if (item.value == '请假') {
+        takingLeave[item.date.split(' ')[0]] = true;
+      }
+    });
+    // 缓存结果
+    localStorage[catchKey] = JSON.stringify(takingLeave);
+  }
+
+  return takingLeave;
+}
+
+/**
+ * 根据时间段查询日志提交情况
+ * @param {moment} startDate
+ * @param {moment} endDate
+ */
+export async function getAllReport(startDate, endDate, onProcess) {
+  if (!access_token) {
+    access_token = await getToken();
+  }
+
+  // 钉钉的接口 URL
+  const url = '/simplelist?access_token=' + access_token;
+
+  const requestData = {
+    start_time: startDate.valueOf(),
+    end_time: endDate.valueOf(),
+    size: 20,
+  };
+
+  // 设置请求头
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: `Bearer ${access_token}`,
+  };
+
+  const getReportByPage = async cursor => {
+    let catchKey = `simplelist|${cursor}|${requestData.start_time}`;
+    let res = {};
+    let catchData = localStorage[catchKey];
+    if (catchData) {
+      res = JSON.parse(catchData);
+    } else {
+      // 发送 POST 请求
+      let response = await api.post(url, { ...requestData, cursor }, { headers });
+      res = response.data.result;
+      res.data_list = res.data_list.map(item => ({
+        create_time: moment(item.create_time).format('YYYY年MM月DD日 HH:mm'),
+        creator_id: item.creator_id,
+        creator_name: item.creator_name,
+      }));
+
+      localStorage[catchKey] = JSON.stringify(res);
+    }
+
+    return res;
+  };
+  let res = null,
+    data_list = [],
+    count = 1;
+  do {
+    res = await getReportByPage(res ? res.next_cursor : 0);
+    console.log(`请求第${count}次。`);
+    onProcess?.(`请求${count * 20}条日志信息。`);
+    data_list = data_list.concat(res.data_list);
+    count++;
+  } while (res.has_more);
+
+  return data_list;
+}
+
+// 查询节假日
+export async function getHoliday(onProcess) {
+  let res,
+    days = {};
+  res = await axios.get(
+    'https://www.mxnzp.com/api/holiday/list/year/2023?ignoreHoliday=false&app_id=kf6mqlkirgupfcok&app_secret=MDRIVy83WTN4Q0lEaUZVMEFGejFWdz09'
+  );
+  console.log(res);
+  res.data.data.forEach(month => {
+    month.days.forEach(item => {
+      if (item.type != 0) {
+        days[item.date] = 1;
+      }
+    });
+  });
+  onProcess?.('查询节假日成功');
+  return days;
+}
+
+// 获取入职时间
+export async function getHiredDate(userId) {
+  if (!access_token) {
+    access_token = await getToken();
+  }
+
+  // 钉钉的接口 URL
+  const url = '/userInfo?access_token=' + access_token;
+
+  const requestData = {
+    userid: userId,
+    language: 'zh_CN',
+  };
+
+  // 设置请求头
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: `Bearer ${access_token}`,
+  };
+  let catchKey = `hiredDate|${userId}`;
+  let hiredDate = localStorage[catchKey];
+
+  // 判断是否缓存过数据
+  if (!hiredDate) {
+    // 发送 POST 请求
+    const response = await api.post(url, requestData, { headers });
+    const time = moment(response.data.result.hired_date).format('YYYY-MM-DD');
+    // 缓存结果
+    hiredDate = localStorage[catchKey] = time;
+  }
+
+  return hiredDate;
+}
+
+// 查询离职日期
+export async function getResignationDate(userIdList) {
+  if (!access_token) {
+    access_token = await getToken();
+  }
+
+  // 设置请求头
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: `Bearer ${access_token}`,
+  };
+
+  const response = await api.get('/dimissionInfos', {
+    headers,
+    params: {
+      access_token,
+      userIdList: JSON.stringify(userIdList),
+    },
+  });
+  let dimissionInfos = {};
+  // 处理响应结果
+  response.data.result.forEach(item => {
+    if (item.status == 2) {
+      dimissionInfos[item.userId] = moment(item.lastWorkDay).format('YYYY-MM-DD');
+    }
+  });
+  // 缓存结果
+
+  return dimissionInfos;
+}
+
+// getAllReport(
+//   moment("2023-04-26 00:00:00"),
+//   moment("2023-05-25 23:59:59"),
+//   "0543113200285"
+// ).then((list) => {
+//   fs.writeFile("report.json", JSON.stringify(list), (err) => {
+//     if (err) {
+//       console.error("写入文件时发生错误:", err);
+//       return;
+//     }
+//     console.log("文本已成功写入report.json文件。");
+//   });
+// });

+ 26 - 4
src/utils/request.js

@@ -7,6 +7,9 @@ import { isAntdPro, getToken, GetTokenFromUrl } from './utils';
 // var apiUrl = "http://oraysmart.com:8888"
 // var apiUrl = "http://120.55.44.4:8900"
 
+//节流阀
+let flag = false;
+let tokenFlag = false;
 const checkStatus = response => {
   if (response.status >= 200 && response.status < 300) {
     return response;
@@ -41,9 +44,9 @@ const checkStatus = response => {
   const error = new Error(response.data);
   error.name = response.status;
   error.response = response;
-  console.error(error)
+  console.error(error);
   // throw error;
-  return Promise.reject()
+  return Promise.reject();
 };
 
 const cachedSave = response => {
@@ -169,13 +172,23 @@ export default function request(url, option, jwt) {
         return response;
       } else if (code !== 200) {
         if (code === 401 || code === 601 || code === 602) {
+          if (tokenFlag) return false;
           // 用户token出错,重定向
+          tokenFlag = true;
+          setTimeout(() => {
+            tokenFlag = false;
+          }, 3000);
           notification.error({
             message: '错误',
             description: 'token失效,请重新登录',
           });
-          router.push('/login');
+          router.push(`/login?referrer=${encodeURIComponent(encodeURIComponent(location.href))}`);
         } else {
+          if (flag) return false;
+          flag = true;
+          setTimeout(() => {
+            flag = false;
+          }, 3000);
           message.error(response.msg);
         }
         return false;
@@ -187,13 +200,22 @@ export default function request(url, option, jwt) {
       const status = e?.name;
       // environment should not be used
       if (status === 401 || status === 601 || status === 602 || status === 400) {
+        if (tokenFlag) return false;
+        // 用户token出错,重定向
+        tokenFlag = true;
+        setTimeout(() => {
+          tokenFlag = false;
+        }, 3000);
         // 用户token出错,重定向
         notification.error({
           message: '错误',
           description: 'token失效,请重新登录',
         });
-        router.push('/login');
+        router.push(`/login?referrer=${encodeURIComponent(encodeURIComponent(location.href))}`);
         return;
+      } else if (status == 'AbortError') {
+        // 中止的ajax 不做额外处理
+        return e;
       }
       // if (status <= 504 && status > 500 && status !== 600) {
       //   router.push('/exception/500');