소스 검색

工时统计初步完工

shaogen1995 5 달 전
부모
커밋
66076aa30c

+ 6 - 3
src/assets/styles/base.css

@@ -8,7 +8,7 @@ html {
   font-size: 14px;
 }
 body {
-  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
+  font: 1em/1.4 "Microsoft Yahei", "PingFang SC", "Avenir", "Segoe UI", "Hiragino Sans GB", "STHeiti", "Microsoft Sans Serif", "WenQuanYi Micro Hei", sans-serif;
   height: 100%;
   color: black;
 }
@@ -35,7 +35,7 @@ textarea {
 }
 /* 主题色 */
 :root {
-  --themeColor: #00A0E6;
+  --themeColor: #00a0e6;
   --themeColor2: #234386;
 }
 /* 找不到页面 */
@@ -176,12 +176,15 @@ textarea {
   left: -18px;
   padding-left: 40px;
 }
+.pageTitle > span {
+  font-weight: 400;
+}
 .pageTitle::before {
   position: absolute;
   left: 20px;
   top: 50%;
   transform: translateY(-50%);
-  content: '';
+  content: "";
   width: 6px;
   height: 20px;
   background-color: var(--themeColor);

+ 11 - 25
src/assets/styles/base.less

@@ -11,13 +11,13 @@ html {
 }
 
 body {
-  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
+  font: 1em/1.4 "Microsoft Yahei", "PingFang SC", "Avenir", "Segoe UI",
+    "Hiragino Sans GB", "STHeiti", "Microsoft Sans Serif", "WenQuanYi Micro Hei",
+    sans-serif;
   height: 100%;
   color: black;
 }
 
-
-
 i {
   font-style: normal;
 }
@@ -46,21 +46,16 @@ textarea {
 
 /* 主题色 */
 :root {
-  --themeColor: #00A0E6;
+  --themeColor: #00a0e6;
   --themeColor2: #234386;
 }
 
-
-
-
-
 /* 找不到页面 */
 .noFindPage {
   opacity: 0;
-  transition: opacity .5s;
+  transition: opacity 0.5s;
 }
 
-
 /* 兼容360浏览器的下拉框 */
 .ant-select-selector {
   position: relative;
@@ -70,7 +65,7 @@ textarea {
 }
 
 // 气泡框闪烁问题
-.ant-popconfirm{
+.ant-popconfirm {
   width: 240px;
 }
 
@@ -137,7 +132,6 @@ textarea {
     background-color: var(--themeColor) !important;
   }
 
-
   .ant-pagination .ant-pagination-item-active a {
     color: #fff !important;
   }
@@ -163,14 +157,11 @@ textarea {
     color: #fff;
   }
 
-
-
   .ant-pagination-next {
     border-radius: 50% !important;
     border: 1px solid #999;
   }
 
-
   .ant-pagination-next:hover {
     background-color: var(--themeColor);
   }
@@ -187,7 +178,6 @@ textarea {
     background-color: transparent;
   }
 
-
   /* 表格的图片居中 */
   .tableImgAuto {
     display: flex;
@@ -205,7 +195,6 @@ textarea {
     text-align: center !important;
   }
 
-
   // 树型 表格 定制化
   #A2Table3 {
     .ant-table-row-expand-icon {
@@ -218,18 +207,12 @@ textarea {
       justify-content: flex-start;
     }
   }
-
-
 }
 
-
-
 [hidden] {
   display: none !important;
 }
 
-
-
 #upInput {
   display: none;
 }
@@ -247,13 +230,16 @@ textarea {
   top: -56px;
   left: -18px;
   padding-left: 40px;
+  & > span {
+    font-weight: 400;
+  }
 
   &::before {
     position: absolute;
     left: 20px;
     top: 50%;
     transform: translateY(-50%);
-    content: '';
+    content: "";
     width: 6px;
     height: 20px;
     background-color: var(--themeColor);
@@ -280,4 +266,4 @@ textarea {
   -webkit-box-shadow: inset 0 0 5px transparent;
   border-radius: 10px;
   background: transparent;
-}
+}

+ 3 - 1
src/components/MyTable/index.tsx

@@ -20,6 +20,7 @@ type Props = {
   myTitle?: { name: string; Com: React.ReactNode };
   // 为空的定制字段
   isNull?: string;
+  rowKey?: string;
 };
 
 // 表格内容定制化
@@ -52,6 +53,7 @@ function MyTable({
   merge,
   myTitle,
   isNull = "(空)",
+  rowKey = "id",
 }: Props) {
   useEffect(() => {
     const dom = document.querySelector(
@@ -140,7 +142,7 @@ function MyTable({
       scroll={{ y: yHeight ? yHeight : "" }}
       dataSource={list}
       columns={[...columns, ...lastBtn]}
-      rowKey="id"
+      rowKey={rowKey}
       pagination={
         pagingInfo
           ? {

+ 86 - 0
src/pages/A2manHour/A2staff/A2sAdd.tsx

@@ -0,0 +1,86 @@
+import React, { useCallback, useEffect, useState } from "react";
+import styles from "./index.module.scss";
+import { Button, Input, Modal } from "antd";
+import { A2sTableType } from ".";
+import MyPopconfirm from "@/components/MyPopconfirm";
+import { MessageFu } from "@/utils/message";
+import { API_A2save } from "@/store/action/all";
+
+type Props = {
+  closeFu: () => void;
+  editFu: () => void;
+  editObj: A2sTableType;
+};
+
+function A2sAdd({ editObj, closeFu, editFu }: Props) {
+  const [name, setName] = useState("");
+
+  const [dept, setDept] = useState("");
+
+  useEffect(() => {
+    setName(editObj.name);
+    setDept(editObj.dept);
+  }, [editObj]);
+
+  const btnOk = useCallback(async () => {
+    if (!name) return MessageFu.warning("姓名不能为空!");
+    if (!dept) return MessageFu.warning("部门不能为空!");
+
+    const obj = {
+      id: editObj.id > 0 ? editObj.id : null,
+      name,
+      dept,
+    };
+    const res = await API_A2save(obj);
+    if (res.code === 0) {
+      MessageFu.success(editObj.id > 0 ? "编辑成功" : "新增成功");
+      editFu();
+      closeFu();
+    }
+  }, [closeFu, dept, editFu, editObj.id, name]);
+
+  return (
+    <Modal
+      wrapClassName={styles.A2sAdd}
+      open={true}
+      title={editObj.id > 0 ? "编辑" : "新增"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="A2sRow">
+        <span className="A2sRowB">*</span>姓名:
+        <Input
+          placeholder="请输入名称"
+          value={name}
+          onChange={(e) => setName(e.target.value.trim())}
+          maxLength={10}
+          showCount
+        />
+      </div>
+
+      <div className="A2sRow">
+        <span className="A2sRowB">*</span>部门:
+        <Input
+          placeholder="请输入部门"
+          value={dept}
+          onChange={(e) => setDept(e.target.value.trim())}
+          maxLength={10}
+          showCount
+        />
+      </div>
+
+      <div className="A2sbtn">
+        <MyPopconfirm txtK="取消" onConfirm={closeFu} />
+        &emsp;
+        <Button type="primary" onClick={btnOk}>
+          提交
+        </Button>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoA2sAdd = React.memo(A2sAdd);
+
+export default MemoA2sAdd;

+ 66 - 0
src/pages/A2manHour/A2staff/index.module.scss

@@ -0,0 +1,66 @@
+.A2staff {
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border-radius: 10px;
+  padding: 20px;
+  :global {
+    .A2sTit {
+      display: flex;
+      justify-content: space-between;
+      .A2sTitll {
+        font-weight: 700;
+        font-size: 18px;
+        span {
+          font-weight: 400;
+        }
+      }
+    }
+  }
+}
+
+// 新增和编辑 人员
+.A2sAdd {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+    .ant-modal {
+      width: 800px !important;
+    }
+
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+      padding-top: 15px !important;
+    }
+
+    .A2sRow {
+      margin-bottom: 20px;
+      .A2sRowB {
+        color: red;
+      }
+    }
+
+    // .A61Mrow {
+    //   display: flex;
+    //   align-items: center;
+    //   margin-bottom: 24px;
+    //   .A61Mrow1 {
+    //     width: 70px;
+    //     text-align: right;
+    //   }
+    //   .A61Mrow2 {
+    //     width: calc(100% - 70px);
+    //     .A61Mrow2Txt {
+    //       font-weight: 700;
+    //       font-size: 30px;
+    //     }
+    //   }
+    // }
+
+    .A2sbtn {
+      margin-top: 24px;
+      text-align: center;
+    }
+  }
+}

+ 113 - 0
src/pages/A2manHour/A2staff/index.tsx

@@ -0,0 +1,113 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import styles from "./index.module.scss";
+import { API_A2del, API_A2getStaffList } from "@/store/action/all";
+import { Button } from "antd";
+import MyTable from "@/components/MyTable";
+import MyPopconfirm from "@/components/MyPopconfirm";
+import { MessageFu } from "@/utils/message";
+import A2sAdd from "./A2sAdd";
+
+type Props = {
+  closeFu: () => void;
+};
+
+export type A2sTableType = {
+  name: string;
+  dept: string;
+  id: number;
+};
+
+function A2staff({ closeFu }: Props) {
+  const [list, setList] = useState<A2sTableType[]>([]);
+
+  const getList = useCallback(async () => {
+    const res = await API_A2getStaffList({
+      pageNum: 1,
+      pageSize: 9999,
+    });
+    if (res.code === 0) {
+      setList(res.data.records);
+    }
+  }, []);
+
+  useEffect(() => {
+    getList();
+  }, [getList]);
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res = await API_A2del(id);
+      if (res.code === 0) {
+        MessageFu.success("删除成功!");
+        getList();
+      }
+    },
+    [getList]
+  );
+
+  //新增和编辑
+  const [editObj, setEditObj] = useState({} as A2sTableType);
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: "操作",
+        render: (item: A2sTableType) => (
+          <>
+            <Button size="small" type="text" onClick={() => setEditObj(item)}>
+              编辑
+            </Button>
+
+            <MyPopconfirm txtK="删除" onConfirm={() => delTableFu(item.id)} />
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu]);
+
+  return (
+    <div className={styles.A2staff}>
+      <div className="A2sTit">
+        <div className="A2sTitll">
+          人员名单- <span>离职或其他原因也暂时需要统计工时的时候填</span>
+        </div>
+        <div>
+          <Button
+            type="primary"
+            onClick={() => setEditObj({ id: -1 } as A2sTableType)}
+          >
+            新增
+          </Button>
+          &emsp;
+          <Button onClick={closeFu}>关闭</Button>
+        </div>
+      </div>
+
+      <MyTable
+        classKey="A2staTale"
+        yHeight={600}
+        list={list}
+        lastBtn={tableLastBtn}
+        columnsTemp={[
+          ["txt", "姓名", "name"],
+          ["txt", "部门", "dept"],
+        ]}
+        pagingInfo={false}
+      />
+
+      {/* 点击新增和编辑 */}
+      {editObj.id ? (
+        <A2sAdd
+          editObj={editObj}
+          closeFu={() => setEditObj({} as A2sTableType)}
+          editFu={getList}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoA2staff = React.memo(A2staff);
+
+export default MemoA2staff;

+ 27 - 0
src/pages/A2manHour/index.module.scss

@@ -1,4 +1,31 @@
 .A2manHour {
+  position: relative;
   :global {
+    .A2top {
+      margin-bottom: 15px;
+      text-align: right;
+    }
+    .tableBox {
+      width: 100%;
+      height: calc(100% - 47px);
+      background-color: #fff;
+      border-radius: 10px;
+
+      .yiTable {
+        color: red;
+      }
+      // .ant-table-cell{
+      //   padding: 4 !important;
+      // }
+    }
+    .StaffBox {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background-color: rgba(0, 0, 0, 0.8);
+      padding: 50px 100px;
+    }
   }
 }

+ 301 - 21
src/pages/A2manHour/index.tsx

@@ -1,34 +1,314 @@
-import React, { useEffect } from "react";
+import React, { useCallback, useMemo, useRef, useState } from "react";
 import styles from "./index.module.scss";
 
 import { isHoliday } from "chinese-days/dist/index.min.js";
+import { MessageFu } from "@/utils/message";
+import { API_upFile } from "@/store/action/layout";
+import { fileDomInitialFu } from "@/utils/domShow";
+import dayjs from "dayjs";
+import ExportJsonExcel from "js-export-excel";
+import { Button } from "antd";
+import MyTable from "@/components/MyTable";
+import { API_A2getList } from "@/store/action/all";
+import A2staff from "./A2staff";
 
-const arr = [
-  "2025-03-07",
-  "2025-03-08",
-  "2025-03-09",
-  "2025-03-22",
-  "2025-03-23",
-  "2025-03-24",
-  "2025-04-01",
-  "2025-04-04",
-  "2025-04-05",
-  "2025-04-06",
-  "2025-04-07",
-  "2025-04-12",
-];
+type listType = {
+  startTime: string;
+  endTime: string;
+  day: number;
+};
+
+type Res2ListType = {
+  account: string;
+  consumed: string;
+  date: string;
+  name: string;
+};
 
 function A2manHour() {
-  useEffect(() => {}, []);
+  // 2个数组数据整理
+  const mergeArrays = useCallback((arr1: any[], arr2: any[]) => {
+    // 创建以姓名为键的临时对象
+    const merged = arr1.reduce((acc, item) => {
+      const converted = Object.entries(item).reduce(
+        (obj: any, [key, value]) => {
+          obj[key] = key === "姓名" ? value : Number(value);
+          return obj;
+        },
+        {}
+      );
+      acc[converted.姓名] = converted;
+      return acc;
+    }, {});
+
+    // 处理第二个数组
+    arr2.forEach((item) => {
+      const existing = merged[item.姓名];
+      const converted = Object.entries(item).reduce(
+        (obj: any, [key, value]) => {
+          obj[key] = key === "姓名" ? value : Number(value);
+          return obj;
+        },
+        {}
+      );
+
+      if (existing) {
+        // 合并相同姓名的数据
+        Object.keys(converted).forEach((key) => {
+          if (key !== "姓名") {
+            existing[key] += converted[key];
+          }
+        });
+      } else {
+        // 添加新条目
+        merged[converted.姓名] = converted;
+      }
+    });
+    // 转换为数组并返回
+    return Object.values(merged);
+  }, []);
+
+  const [list, setList] = useState<listType[]>([]);
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  const [biaoTou, setBiaoTou] = useState<string[]>([]);
+
+  // 根据表头来算出表格需要的数据
+  const tableTouRes = useMemo(() => {
+    let arr: any = [
+      // ["txt", "节假日", "isHoliday"],
+      // ["txt", "异常", "anomaly"],
+    ];
+    biaoTou.forEach((v) => {
+      if (v === "姓名") {
+        arr.push(["txt", v, v]);
+      } else {
+        arr.push([
+          "txt",
+          isHoliday(v) ? <span className="yiTable">{v}(假)</span> : v,
+          v,
+        ]);
+      }
+    });
+
+    return arr;
+  }, [biaoTou]);
+
+  const dataChangeFu = useCallback(
+    async (resData: any) => {
+      //  表头
+      let touArr = resData[0].map((v: string) => v.replace(" 00:00:00", ""));
+
+      if (touArr.length < 3) return MessageFu.warning("最少填入2个日期");
+      if (touArr.length > 11) return MessageFu.warning("最多填入10个日期");
+
+      //   [
+      //     "姓名",
+      //     "2025-03-05",
+      //     "2025-03-06",
+      //     "2025-03-07",
+      //     "2025-03-08",
+      //     "2025-03-09",
+      //     "2025-03-10",
+      //     "2025-03-11",
+      //     "2025-03-12",
+      //     "2025-03-13",
+      //     "2025-03-14"
+      // ]
+
+      // 拿到开始时间和结束时间
+      const objTime = {
+        startTime: touArr[1] + " 00:00:00",
+        endTime: touArr[touArr.length - 1] + " 23:59:59",
+      };
+
+      const res = await API_A2getList(objTime);
+
+      if (res.code === 0) {
+        const resData2Temp: Res2ListType[] = res.data;
+
+        const res2ttArr: any[] = [];
+
+        const res2ttObj: any = {};
+
+        resData2Temp.forEach((v) => {
+          if (res2ttObj[v.name]) {
+            res2ttObj[v.name].push(v);
+          } else res2ttObj[v.name] = [v];
+        });
+
+        for (const k in res2ttObj) {
+          res2ttObj[k] = res2ttObj[k].sort(
+            (a: any, b: any) =>
+              dayjs(a.date).valueOf() - dayjs(b.date).valueOf()
+          );
+          const objc: any = { 姓名: k };
+          res2ttObj[k].forEach((c: any) => {
+            objc[c.date] = Number(c.consumed);
+          });
+          res2ttArr.push(objc);
+        }
+
+        setBiaoTou(touArr);
+
+        const resArr1: any = [];
+        const arr2: any = resData.slice(1);
+
+        arr2.forEach((v1: string[], i1: number) => {
+          let obj: any = {};
+
+          touArr.forEach((v2: string, i2: number) => {
+            obj[v2] = v1[i2] ? v1[i2] : "0";
+          });
+
+          resArr1.push(obj);
+        });
+
+        const resList: any[] = mergeArrays(resArr1, res2ttArr);
+
+        // 异常的处理
+        resList.forEach((v) => {
+          for (const k in v) {
+            if (k !== "姓名") {
+              let flag = isHoliday(k);
+              if (flag) {
+                if (v[k]) v[k] = v[k] + "(异)";
+              } else {
+                if (v[k] < 8) v[k] = v[k] + "(异)";
+              }
+            }
+          }
+        });
+
+        setList(resList);
+      }
+    },
+    [mergeArrays]
+  );
+
+  // 上传表格
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        // 校验格式
+        // const type = format;
+        if (!filesInfo.name.includes(".xlsx"))
+          return MessageFu.warning("只支持.xlsx文件!");
+
+        // 创建FormData对象
+        const fd = new FormData();
+        fd.append("type", "doc");
+        fd.append("dirCode", "A1Day");
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        try {
+          const res = await API_upFile(fd, "cms/taskTime/upload");
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            // setFileUrl(res.data);
+            dataChangeFu(res.data || []);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dataChangeFu]
+  );
+
+  // 点击导出
+  const deriveFu = useCallback(async () => {
+    // if (list.length <= 0) return;
+
+    const name = "工时统计" + dayjs(new Date()).format("YYYY-MM-DD HH:mm");
+
+    // console.log(123, list, biaoTou);
+
+    const option = {
+      fileName: name,
+      datas: [
+        {
+          sheetData: list,
+          sheetName: name,
+          sheetFilter: biaoTou,
+          sheetHeader: biaoTou.map((v) =>
+            v !== "姓名" ? (isHoliday(v) ? v + "(假)" : v) : v
+          ),
+          columnWidths: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
+        },
+      ],
+    };
+
+    const toExcel = new ExportJsonExcel(option); //new
+    toExcel.saveExcel(); //保存
+  }, [biaoTou, list]);
+
+  // 人员名单
+  const [staffShow, setStaffShow] = useState(false);
 
   return (
     <div className={styles.A2manHour}>
-      {arr.map((v) => (
-        <div key={v}>
-          <span>{v}</span>
-          {isHoliday(v) ? "节假日" : "工作日"}
+      <input
+        id="upInput"
+        type="file"
+        accept=".xlsx"
+        ref={myInput}
+        onChange={(e) => handeUpPhoto(e)}
+      />
+      <div className="pageTitle">
+        工时统计-
+        <span>目前节假日只更新到2025年底。2026年之后请联系开发迭代</span>
+      </div>
+
+      <div className="A2top">
+        <Button type="primary" onClick={() => setStaffShow(true)}>
+          人员名单
+        </Button>
+        &emsp;&emsp;
+        {list.length ? (
+          <>
+            <Button className="A2btn" onClick={() => setList([])}>
+              清空
+            </Button>
+            &emsp;&emsp;
+            <Button type="primary" onClick={deriveFu}>
+              导出表格
+            </Button>
+          </>
+        ) : (
+          <Button
+            type="primary"
+            className="A2btn"
+            onClick={() => myInput.current?.click()}
+          >
+            上传表格
+          </Button>
+        )}
+      </div>
+
+      <div className="tableBox">
+        <MyTable
+          yHeight={707}
+          list={list}
+          columnsTemp={tableTouRes}
+          pagingInfo={false}
+          rowKey="姓名"
+          isNull="0"
+        />
+      </div>
+
+      {staffShow ? (
+        <div className="StaffBox">
+          <A2staff closeFu={() => setStaffShow(false)} />
         </div>
-      ))}
+      ) : null}
     </div>
   );
 }

+ 27 - 5
src/store/action/all.ts

@@ -1,9 +1,31 @@
 // -------------------计算工时模块--------------------------
 
+import http from "@/utils/http";
+
+/**
+ * 获取全公司人的工时
+ */
+export const API_A2getList = (data: any) => {
+  return http.post("cms/taskTime/getAllTaskestimate", data);
+};
+
+/**
+ * 获取人员名单
+ */
+export const API_A2getStaffList = (data: any) => {
+  return http.post("cms/zdUser/page", data);
+};
+
+/**
+ * 人员名单-删除
+ */
+export const API_A2del = (id: number) => {
+  return http.get(`cms/zdUser/remove/${id}`);
+};
+
 /**
- * 修改密码接口
+ * 人员名单新增/编辑
  */
-// export const passWordEditAPI = (data: any) => {
-//   return http.post("sys/user/updatePwd", { ...data });
-// };
-export const aa = 1;
+export const API_A2save = (data: any) => {
+  return http.post("cms/zdUser/save", data);
+};