Renxy преди 1 година
родител
ревизия
0d20bb2a76
променени са 47 файла, в които са добавени 3379 реда и са изтрити 382 реда
  1. 66 24
      .umirc.ts
  2. 2 0
      package.json
  3. 50 3
      src/pages/PurchaseList/Approval/ApprovalModal.js
  4. 13 11
      src/pages/PurchaseList/Approval/Auth.js
  5. 7 18
      src/pages/PurchaseList/Approval/AuthModal.js
  6. 7 12
      src/pages/PurchaseList/Approval/BudgetModal.js
  7. 398 17
      src/pages/PurchaseList/Approval/DetailModal.js
  8. 1 1
      src/pages/PurchaseList/Approval/DetailModal.less
  9. 236 123
      src/pages/PurchaseList/Approval/List.js
  10. 330 0
      src/pages/PurchaseList/Approval/ManufacturerModal.js
  11. 54 50
      src/pages/PurchaseList/Approval/MemberModal.js
  12. 123 0
      src/pages/PurchaseList/Approval/ProjectRecordModal.js
  13. 131 0
      src/pages/PurchaseList/Approval/Statistic.js
  14. 54 0
      src/pages/PurchaseList/Approval/StatusRender.js
  15. 89 0
      src/pages/PurchaseList/Approval/TableRender.js
  16. 105 0
      src/pages/PurchaseList/Approval/index.less
  17. 13 1
      src/pages/PurchaseList/Approval/models/approval.js
  18. 57 0
      src/pages/PurchaseList/DailyRecord/components/RecordDetail.js
  19. 185 0
      src/pages/PurchaseList/DailyRecord/components/WriteRecordModal.js
  20. 40 0
      src/pages/PurchaseList/DailyRecord/components/index.less
  21. 273 0
      src/pages/PurchaseList/DailyRecord/index.js
  22. 6 0
      src/pages/PurchaseList/DailyRecord/index.less
  23. 83 0
      src/pages/PurchaseList/DailyRecord/models/record.js
  24. 4 4
      src/pages/PurchaseList/Report/DepCompareModal.js
  25. 1 1
      src/pages/PurchaseList/Report/Department.js
  26. 1 1
      src/pages/PurchaseList/Report/Finance.js
  27. 2 2
      src/pages/PurchaseList/Report/Finance/Project.js
  28. 42 20
      src/pages/PurchaseList/Report/Project.js
  29. 1 1
      src/pages/PurchaseList/Report/ProjectTree.js
  30. 33 11
      src/pages/PurchaseList/Report/UserProjectRptModal.js
  31. 9 3
      src/pages/PurchaseList/Report/UserRptModal.js
  32. 5 3
      src/pages/PurchaseList/Report/models/report.js
  33. 2 2
      src/pages/PurchaseList/WorkingHours/AddModal.js
  34. 17 16
      src/pages/PurchaseList/WorkingHours/Auth.js
  35. 8 8
      src/pages/PurchaseList/WorkingHours/CalendarModal.js
  36. 2 2
      src/pages/PurchaseList/WorkingHours/RejectModal.js
  37. 16 12
      src/pages/PurchaseList/WorkingHours/SearchForm.js
  38. 43 27
      src/pages/PurchaseList/WorkingHours/index.js
  39. 7 6
      src/pages/PurchaseList/WorkingHours/models/workingHours.js
  40. 101 0
      src/pages/ReportDaily/Index.js
  41. 148 0
      src/pages/ReportDaily/components/ReportTable.js
  42. 170 0
      src/pages/ReportDaily/utils.js
  43. 50 0
      src/services/Daily.js
  44. 310 0
      src/services/ReportDaily.js
  45. 7 2
      src/services/approval.js
  46. 69 0
      src/services/record.js
  47. 8 1
      yarn.lock

+ 66 - 24
.umirc.ts

@@ -59,7 +59,7 @@ export default defineConfig({
   proxy: {
     '/api': {
       // target: 'http://192.168.20.232:1007/',
-      target: 'http://47.96.12.136:8895/',
+      target: 'http://47.96.12.136:8888/',
       // target: 'https://work.greentech.com.cn/',
       changeOrigin: true,
     },
@@ -200,24 +200,25 @@ export default defineConfig({
           path: 'work-hours-auth',
           component: './PurchaseList/WorkingHours/Auth',
         },
-        {
-          name: '工时报表',
-          path: '/workload/report',
-          icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
-          routes: [
-            {
-              name: '项目报表',
-              path: 'project',
-              component: './PurchaseList/Report/ProjectTree',
-            },
-            {
-              name: '部门报表',
-              path: 'department',
-              component: './PurchaseList/Report/Department',
-            },
-            { name: '财务报表', path: '/workload/report/finance' },
-          ],
-        },
+        // {
+        //   name: '工时报表',
+        //   path: '/workload/report',
+        //   icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+        //   routes: [
+        //     {
+        //       name: '项目报表',
+        //       path: 'project',
+        //       component: './PurchaseList/Report/ProjectTree',
+        //     },
+        //     {
+        //       name: '部门报表',
+        //       path: 'department',
+        //       component: './PurchaseList/Report/Department',
+        //     },
+        //     // { name: '财务报表',
+        //     // path: 'finance', path: '/workload/report/finance' },
+        //   ],
+        // },
       ],
     },
     {
@@ -237,20 +238,61 @@ export default defineConfig({
         },
       ],
     },
+    {
+      name: '工时报表',
+      path: '/workload/report',
+      icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+      routes: [
+        {
+          name: '项目报表',
+          path: 'project',
+          component: './PurchaseList/Report/ProjectTree',
+        },
+        {
+          name: '部门报表',
+          path: 'department',
+          component: './PurchaseList/Report/Department',
+        },
+        {
+          name: '财务报表',
+          path: '/workload/report/finance',
+          // icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+          routes: [
+            {
+              name: '资源总表',
+              path: 'index',
+              component: './PurchaseList/Report/Finance',
+            },
+            {
+              name: '资源中心人日使用汇总表',
+              path: 'resources',
+              component: './PurchaseList/Report/Finance/Resources',
+            },
+            {
+              name: '执行项目人日汇总表',
+              path: 'project',
+              component: './PurchaseList/Report/Finance/Project',
+            },
+          ],
+        },
+      ],
+    },
     {
       name: '人日日志',
-      path: '/workload',
+      path: '/workload/record',
       icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
       routes: [
         {
           name: '项目日志',
-          path: '/workload',
-          icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+          path: 'project',
+          component: './PurchaseList/DailyRecord',
+          // icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
         },
         {
           name: '个人日志',
-          path: '/workload/approval',
-          icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+          path: 'report-daily',
+          component: './ReportDaily/Index',
+          // icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
         },
         // {
         //   name: '工时报表',

+ 2 - 0
package.json

@@ -20,6 +20,8 @@
     "echarts": "^5.4.2",
     "exceljs": "^4.3.0",
     "file-saver": "^2.0.5",
+    "lodash": "^4.17.21",
+    "lodash-decorators": "^6.0.1",
     "luckyexcel": "^1.0.1",
     "md5": "^2.3.0",
     "moment": "^2.29.4",

+ 50 - 3
src/pages/PurchaseList/Approval/ApprovalModal.js

@@ -1,8 +1,9 @@
 import React, { useState, useEffect } from 'react';
-import { Form, Select, Modal, Input, TreeSelect } from 'antd';
-import moment from 'moment';
+import { Form, Select, Modal, Input, TreeSelect, Button } from 'antd';
 import provinces from './provinces';
 import { queryApproval } from '@/services/approval';
+import FirmModal from './ManufacturerModal';
+import TableRender from './TableRender';
 const { Option } = Select;
 const { TreeNode } = TreeSelect;
 // 新建
@@ -21,6 +22,8 @@ function AddModal(props) {
     typeList = [],
     disabled,
     loading,
+    supplierList = [],
+    onAddFirm,
   } = props;
   const [form] = Form.useForm();
   const [codes, setCodes] = useState({
@@ -31,6 +34,7 @@ function AddModal(props) {
     version: '',
   });
   const [type, setType] = useState({});
+  const [addFirmVisible, setAddFirmVisible] = useState(false);
 
   const handleOk = () => {
     form.validateFields().then((fieldsValue) => {
@@ -56,6 +60,10 @@ function AddModal(props) {
       } else {
         values.author = null;
       }
+      const supplierName = supplierList.find(
+        (item) => item.id == fieldsValue.supplier_id,
+      )?.name;
+      if (supplierName) values.supplier_name = supplierName;
       onOk(values);
     });
   };
@@ -144,6 +152,45 @@ function AddModal(props) {
             ))}
           </Select>
         </Form.Item>
+        <div style={{ position: 'relative' }}>
+          <Form.Item
+            label="项目客户"
+            name="supplier_id"
+            initialValue={data.supplier_id}
+            rules={[{ required: true, message: '请选择项目客户' }]}
+          >
+            <Select
+              style={{ width: '100%' }}
+              onChange={(e) => form.setFieldsValue({ supplier_id: e })}
+              showSearch
+              filterOption={(input, option) => {
+                return (option?.children[0] ?? '').includes(input);
+              }}
+            >
+              {supplierList.map((item) => (
+                <Option key={item.id} value={Number(item.id)}>
+                  {item.name}({item.id})
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Button
+            style={{ position: 'absolute', right: ' -18px', top: '0' }}
+            type="primary"
+            onClick={onAddFirm}
+          >
+            新建客户
+          </Button>
+        </div>
+
+        <Form.Item
+          label="项目规模"
+          name="process_info"
+          initialValue={data.process_info}
+          rules={[{ required: true, message: '请填写项目规模' }]}
+        >
+          <TableRender />
+        </Form.Item>
         <Form.Item
           label="项目地区"
           name="location"
@@ -228,7 +275,7 @@ function AddModal(props) {
       confirmLoading={loading}
       maskClosable={false}
       destroyOnClose
-      open={visible}
+      visible={visible}
       onCancel={onClose}
       onOk={handleOk}
     >

+ 13 - 11
src/pages/PurchaseList/Approval/Auth.js

@@ -9,13 +9,13 @@ import {
   Input,
   Select,
 } from 'antd';
-import moment from 'moment';
+import dayjs from 'dayjs';
 import styles from './List.less';
 import AuthModal from './AuthModal';
 import DetailModal from './DetailModal';
 import RejectModal from '../WorkingHours/RejectModal';
 import { connect } from 'dva';
-import { useRequest, useModel } from '@umijs/max';
+import { useModel } from '@umijs/max';
 
 const { Option } = Select;
 //状态
@@ -39,11 +39,10 @@ const STATUS = [
 ];
 
 function Auth(props) {
-  const {
-    initialState: { user },
-  } = useModel('@@initialState');
   const { industryList, typeList, data, flowList, depRole, dispatch, loading } =
     props;
+  const { initialState } = useModel('@@initialState');
+  const currentUser = initialState?.user || {};
   const [form] = Form.useForm();
   const [visible, setVisible] = useState(false);
   const [detailVisible, setDetailVisible] = useState(false);
@@ -145,7 +144,7 @@ function Auth(props) {
     {
       title: '创建时间',
       dataIndex: 'c_time',
-      render: (c_time) => moment(c_time).format('YYYY.MM.DD'),
+      render: (c_time) => dayjs(c_time).format('YYYY.MM.DD'),
     },
     {
       title: '执行经理',
@@ -185,8 +184,10 @@ function Auth(props) {
     let flow = flowList.find((item) => item.id == NodeInfo.flow_id);
     if (!flow) return false;
 
-    if (project_status == 2) return currentItem.opt_manager_id == user.ID;
-    if (project_status == 3) return currentItem.wty_manager_id == user.ID;
+    if (project_status == 2)
+      return currentItem.opt_manager_id == currentUser.ID;
+    if (project_status == 3)
+      return currentItem.wty_manager_id == currentUser.ID;
 
     let { NodeAudits } = flow.Nodes.find((item) => item.id == NodeInfo.id);
 
@@ -314,12 +315,12 @@ function Auth(props) {
   }, []);
 
   useEffect(() => {
-    if (!user.ID) return;
+    if (!currentUser.ID) return;
     dispatch({
       type: 'user/queryDepRole',
-      payload: user,
+      payload: currentUser,
     });
-  }, [user]);
+  }, [currentUser]);
 
   return (
     <div>
@@ -360,6 +361,7 @@ export default connect(({ approval, user, loading }) => ({
   typeList: approval.typeList,
   flowList: approval.flowList,
   industryList: approval.industryList,
+  currentUser: user.currentUser,
   depRole: user.depRole,
   loading: loading.models.approval,
 }))(Auth);

+ 7 - 18
src/pages/PurchaseList/Approval/AuthModal.js

@@ -4,28 +4,19 @@ import { Button, Form, Modal, Steps } from 'antd';
 const { Step } = Steps;
 // 新建
 function AuthModal(props) {
-  const {
-    visible,
-    onClose,
-    onAuth,
-    form,
-    data,
-    flowList = [],
-    canAuth,
-    loading,
-  } = props;
+  const { visible, onClose, onAuth, form, data, flowList = [], canAuth, loading } = props;
 
   const flow = useMemo(() => {
     if (!data.flow_id) return {};
-    return flowList.find((item) => item.id == data.flow_id) || {};
+    return flowList.find(item => item.id == data.flow_id) || {};
   }, [flowList, data]);
 
   const current = useMemo(() => {
     if (!data.node_id) return 0;
-    return flow.Nodes?.findIndex((item) => item.id == data.node_id);
+    return flow.Nodes?.findIndex(item => item.id == data.node_id);
   }, [flowList, data]);
 
-  const getAudits = (nodeInfo) => {
+  const getAudits = nodeInfo => {
     switch (nodeInfo.id) {
       case 11:
         return '执行项目经理';
@@ -36,9 +27,7 @@ function AuthModal(props) {
       case 14:
         return '质保经理';
       default:
-        return (nodeInfo.NodeAudits || [])
-          .map((item) => item.AuthorRoleInfo.Name)
-          .join(',');
+        return (nodeInfo.NodeAudits || []).map(item => item.AuthorRoleInfo.Name).join(',');
     }
   };
 
@@ -64,13 +53,13 @@ function AuthModal(props) {
     <Modal
       title="审核详情"
       width={800}
-      open={visible}
+      visible={visible}
       onCancel={onClose}
       footer={renderFooter()}
     >
       <Steps current={current}>
         {/* <Steps current={data?.node_id}> */}
-        {(flow.Nodes || []).map((item) => (
+        {(flow.Nodes || []).map(item => (
           <Step
             key={item.id}
             title={item.node}

+ 7 - 12
src/pages/PurchaseList/Approval/BudgetModal.js

@@ -3,8 +3,7 @@ import { Form, Modal, InputNumber } from 'antd';
 import { connect } from 'dva';
 
 function BudgetModal(props) {
-  const { visible, onCancel, onOk, loading, currentItem, dispatch, budget } =
-    props;
+  const { visible, onCancel, onOk, loading, currentItem, dispatch, budget } = props;
   const [form] = Form.useForm();
 
   useEffect(() => form.resetFields(), [visible]);
@@ -30,18 +29,18 @@ function BudgetModal(props) {
   ];
 
   const handleOk = () => {
-    form.validateFields().then((values) => {
+    form.validateFields().then(values => {
       console.log(values);
       let params = [];
       let isUpdate = budget?.length !== 0;
-      Object.keys(values).forEach((item) => {
+      Object.keys(values).forEach(item => {
         let elm = {
           project_id: Number(currentItem?.id),
           type_id: Number(item),
           workload: Number(values[item]),
         };
         if (isUpdate) {
-          elm.id = budget?.find((child) => child.type_id == item)?.id;
+          elm.id = budget?.find(child => child.type_id == item)?.id;
         }
         params.push(elm);
       });
@@ -56,7 +55,7 @@ function BudgetModal(props) {
   return (
     <Modal
       title="项目预算"
-      open={visible}
+      visible={visible}
       width={800}
       onOk={handleOk}
       onCancel={onCancel}
@@ -64,15 +63,11 @@ function BudgetModal(props) {
       destroyOnClose
     >
       <Form form={form} labelCol={{ span: 10 }} wrapperCol={{ span: 10 }}>
-        {subTypeList.map((item) => (
+        {subTypeList.map(item => (
           <Form.Item
             label={item.name}
             name={item.id}
-            initialValue={
-              Number(
-                budget?.find((child) => child.type_id === item.id)?.workload,
-              ) || 0
-            }
+            initialValue={Number(budget?.find(child => child.type_id === item.id)?.workload) || 0}
           >
             <InputNumber min={0} style={{ width: '100%' }} />
           </Form.Item>

+ 398 - 17
src/pages/PurchaseList/Approval/DetailModal.js

@@ -1,11 +1,33 @@
-import React, { useState, useEffect, useMemo } from 'react';
-import { Form, Modal, Steps } from 'antd';
+import React, { useState, useEffect, useMemo, useRef } from 'react';
+import { Button, Form, Modal, Select, Steps, Tabs, TreeSelect, Upload, message } from 'antd';
 import styles from './DetailModal.less';
+import TableRender from './TableRender';
+import MemberModal from './MemberModal';
+import StatusRender from './StatusRender';
+import { STATUS, SUB_STATUS } from './List';
+import { async } from '@antv/x6/lib/registry/marker/async';
+import { queryStatusHistory } from '@/services/approval';
+import { UploadOutlined } from '@ant-design/icons';
+import { startExecution, startOperate, startQuality } from '@/services/approval';
+import { connect } from 'dva';
 
 const { Step } = Steps;
 // 新建
 function DetailModal(props) {
-  const { visible, onClose, onOk, form, data, flowList = [], disabled, loading } = props;
+  const {
+    visible,
+    onClose,
+    depUserTree,
+    onOk,
+    form,
+    data,
+    isEdit,
+    flowList = [],
+    disabled,
+    currentUser,
+    loading,
+    dispatch,
+  } = props;
   const [codes, setCodes] = useState({
     type: '',
     industry: '',
@@ -13,6 +35,8 @@ function DetailModal(props) {
     name: '',
     version: '',
   });
+  const [params, setParams] = useState({});
+  const [statusList, setStatusHistory] = useState([]);
 
   useEffect(() => {
     if (!visible || !data.id) return;
@@ -23,11 +47,313 @@ function DetailModal(props) {
       name: data.name,
       version: data.version,
     });
+    // setParams({
+    //   project_status: data?.project_status,
+    //   // status: data?.status,
+    // });
+    if (data?.id) initStatueHistory(data.id);
   }, [data, visible]);
 
-  const renderDetail = () => (
-    <>
-      <div className={styles.subTitle}>项目详情</div>
+  const canEdit = useMemo(() => {
+    if (!data || !currentUser) return false;
+    let { audit_status, project_status } = data;
+
+    if (currentUser.IsSuper) return true;
+    switch (audit_status) {
+      case 0:
+        return currentUser.ID == data.author;
+      case 1:
+        return false;
+      case 2:
+        if (project_status == 0) return currentUser.ID == data.author;
+        if (project_status == 1) return currentUser.ID == data.LeaderId;
+        return false;
+      case 3:
+        switch (project_status) {
+          case 0:
+            return currentUser.ID == data.author;
+          case 1:
+            return currentUser.ID == data.LeaderId;
+          case 2:
+            return currentUser.ID == data.LeaderId || currentUser.ID == data.opt_manager_id;
+          case 3:
+            return currentUser.ID == data.LeaderId || currentUser.ID == data.wty_manager_id;
+        }
+        return false;
+    }
+  }, [currentUser, data, isEdit]);
+  const promise = useMemo(() => {
+    if (!data || !currentUser) return false;
+    let { audit_status, project_status, type_id } = data;
+    console.log('============', data);
+    let permission = currentUser.Permission;
+    let showModifyManager = false;
+    let showEditBtn = false;
+    let showMember = false;
+    let showStatus = false;
+    let showExecution = false;
+    if (isEdit) {
+      if (
+        (project_status === 0 || project_status === 1) &&
+        (permission['func-01-point-pm-list-change'] || currentUser.IsSuper)
+      ) {
+        showModifyManager = true;
+      }
+
+      switch (audit_status) {
+        //审核拒绝
+        case 2:
+          if (project_status == 0 && canEdit) showEditBtn = true;
+          else if (project_status == 1 && canEdit) {
+            showMember = true;
+            showStatus = true;
+          }
+          break;
+        //审核通过
+        case 3:
+          switch (project_status) {
+            //售前
+            case 0:
+              if (canEdit) {
+                showMember = true;
+                showExecution = true;
+              }
+              break;
+            //转执行
+            case 1:
+              if (canEdit) {
+                showMember = true;
+                showStatus = true;
+              }
+              break;
+            //转运营
+            case 2:
+            case 3:
+              if (canEdit) {
+                showMember = true;
+              }
+              break;
+          }
+          break;
+      }
+    }
+
+    return { showModifyManager, showEditBtn, showMember, showStatus, showExecution };
+  }, [currentUser, data, isEdit]);
+  const manager = useMemo(() => {
+    let manager = {};
+    switch (data.project_status) {
+      case 0:
+        // 售前
+        manager = data.AuditUser || {};
+        manager.DepID = data.author_dep_id;
+        break;
+      case 1:
+        // 执行
+        manager = data.Leader || {};
+        manager.DepID = data.leader_dep_id;
+        break;
+      case 2:
+        // 运营
+        manager = data.OptManager || {};
+        manager.DepID = data.opt_manager_dep_id;
+        break;
+      case 3:
+        // 质保
+        manager = data.WtyManager || {};
+        manager.DepID = data.wty_manager_dep_id;
+        break;
+    }
+    return manager;
+  }, [data]);
+  const statusOptions = useMemo(() => {
+    if (!data?.id) return [];
+    const { showStatus, showExecution } = promise;
+    const execution = showExecution ? { value: 11, label: '转执行' } : null;
+    const ops = showStatus ? { value: 21, label: '转运营' } : null;
+    const qualityOperate = showStatus ? { value: 31, label: '转质保' } : null;
+    let subSelectOptions = {
+      0: [
+        { value: 1, label: '初步交流' },
+        { value: 42, label: '放弃' },
+        { value: 41, label: '失败' },
+        { value: 43, label: '关闭' },
+      ],
+      1: [
+        { value: 2, label: '预算和方式' },
+        { value: 42, label: '放弃' },
+        { value: 41, label: '失败' },
+        { value: 43, label: '关闭' },
+      ],
+      2: [
+        { value: 3, label: '招标' },
+        { value: 42, label: '放弃' },
+        { value: 41, label: '失败' },
+        { value: 43, label: '关闭' },
+      ],
+      3: [
+        { value: 4, label: '中标' },
+        { value: 41, label: '失败' },
+        { value: 43, label: '关闭' },
+      ],
+      4: [execution, { value: 43, label: '关闭' }],
+      // 11 转执行
+      11: [ops, qualityOperate, { value: 43, label: '关闭' }],
+      // 21 转运营
+      21: [qualityOperate, { value: 43, label: '关闭' }],
+      // 31 转质保
+      31: [{ value: 43, label: '关闭' }],
+      // 41 失败
+      // 42 放弃
+      // 43 关闭
+    };
+
+    return (subSelectOptions[data.status] || []).filter(item => item); // 过滤无权限选项
+  }, [promise]);
+
+  const renderManage = () => {
+    let label = '';
+    if (params.status == 11) {
+      label = '项目经理';
+    } else if (params.status == 21) {
+      label = '运营经理';
+    } else if (params.status == 31) {
+      label = '质保经理';
+    }
+    if (!label) return null;
+    return (
+      <>
+        <Form.Item label={label} rules={[{ required: true, message: `请选择${label}` }]}>
+          <TreeSelect
+            showSearch
+            allowClear
+            placeholder={`请选择${label}`}
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title.includes(input);
+            }}
+            onChange={value => setParams({ ...params, Manager: value })}
+            treeData={depUserTree}
+          />
+        </Form.Item>
+        {params.status == 11 && (
+          <Form.Item label="合同状态">
+            <Select
+              onChange={value => setParams({ ...params, contractStatus: value })}
+              placeholder="请选择有无合同"
+            >
+              <Option key={0}>无合同</Option>
+              <Option key={1}>有合同</Option>
+            </Select>
+          </Form.Item>
+        )}
+      </>
+    );
+  };
+
+  const showUpload = useMemo(() => {
+    if (!isEdit) return false;
+    if (params.status == 2 || params.status == 3 || params.status == 4 || params.status == 11)
+      return true;
+  }, [params, isEdit]);
+
+  const initStatueHistory = async id => {
+    const res = await queryStatusHistory({ id });
+    if (res.data) setStatusHistory(res.data);
+  };
+
+  const handleSubChange = e => {
+    console.log(e);
+    setParams({ ...params, status: e });
+  };
+
+  const onSave = () => {
+    let type = '';
+    let payload = null;
+    if (params.status == 11) {
+      // '项目经理'
+      if (!params.Manager) return message.error('请选择项目经理');
+      if (!params.contractStatus) return message.error('请选择有无合同');
+      type = 'approval/startExecution';
+      const [manager_id, dep_id] = params.Manager.split('||');
+      payload = {
+        project_code_id: data.id,
+        with_contract: Number(params.contractStatus),
+        dep_id: Number(dep_id),
+        exe_manager_id: Number(manager_id),
+      };
+      if (params.attach && params.attach.length > 0) {
+        try {
+          payload.attach = JSON.stringify(params.attach);
+        } catch (e) {
+          console.error(e);
+          payload.attach = '';
+        }
+      }
+    } else if (params.status == 21) {
+      // '运营经理'
+      if (!params.Manager) return message.error('请选择运营经理');
+      type = 'approval/startOperate';
+      const [manager_id, dep_id] = params.Manager.split('||');
+      payload = {
+        project_code_id: data.id,
+        dep_id: Number(dep_id),
+        opt_manager_id: Number(manager_id),
+      };
+    } else if (params.status == 31) {
+      // '质保经理'
+      type = 'approval/startQuality';
+      const [manager_id, dep_id] = params.Manager.split('||');
+      payload = {
+        project_code_id: data.id,
+        dep_id: Number(dep_id),
+        wty_manager_id: Number(manager_id),
+      };
+    } else {
+      type = 'approval/updateApproval';
+      payload = {
+        ...data,
+        ...params,
+      };
+      // 判断是否修改过项目经理
+      if (!(promise.showModifyManager && params.manager_id)) {
+        // 未修改过项目经理,则项目经理使用当前的
+        payload.manager_id = manager.ID;
+      }
+      if (params.attach && params.attach.length > 0) {
+        try {
+          payload.attach = JSON.stringify(params.attach);
+        } catch (e) {
+          console.error(e);
+          payload.attach = '';
+        }
+      }
+    }
+    dispatch({
+      type,
+      payload,
+      callback: () => onClose(),
+    });
+  };
+
+  const uploadProps = {
+    name: 'files',
+    multiple: true,
+    action: `/api/v2/approval/attach`,
+    headers: {
+      'JWT-TOKEN': localStorage.getItem('JWT-TOKEN'),
+    },
+    // defaultFileList: attachData?.attach_extend,
+    onChange({ file, fileList }) {
+      if (file.status !== 'uploading') {
+        const urlList = fileList.map(item => item.response?.data);
+        setParams({ ...params, attach: urlList });
+      }
+    },
+  };
+
+  const renderDetail = () => {
+    return (
       <Form labelCol={{ span: 4 }} wrapperCol={{ span: 18 }}>
         <Form.Item className={styles.formItem} label="项目名称">
           {data.project_name}
@@ -56,11 +382,25 @@ function DetailModal(props) {
             </Form.Item>
           </>
         )}
-        {data.AuthorUser && (
-          <Form.Item className={styles.formItem} label="售前项目经理">
-            {data.AuthorUser.CName}
-          </Form.Item>
-        )}
+        <Form.Item className={styles.formItem} label="项目经理">
+          {!promise.showModifyManager ? (
+            manager?.CName
+          ) : (
+            <TreeSelect
+              // defaultValue={`${manager.ID}||${manager.DepID}`}
+              placeholder={manager.CName || '请选择项目经理'}
+              showSearch
+              allowClear
+              style={{ width: '60%' }}
+              multiple={false}
+              filterTreeNode={(input, option) => {
+                return option.props.title === input;
+              }}
+              onChange={value => setParams({ ...params, manager_id: Number(value.split('||')[0]) })}
+              treeData={depUserTree}
+            />
+          )}
+        </Form.Item>
         {data.AuthorDepInfo && (
           <Form.Item className={styles.formItem} label="所属部门">
             {data.AuthorDepInfo.Name}
@@ -81,9 +421,41 @@ function DetailModal(props) {
             {data.OptManager.CName}
           </Form.Item>
         )}
+        <Form.Item className={styles.formItem} label="项目阶段">
+          {STATUS.find(item => item.value == data?.project_status)?.label}
+        </Form.Item>
+        <Form.Item className={styles.formItem} label="现阶段状态">
+          {isEdit ? (
+            <Select
+              // defaultValue={SUB_STATUS[0].label}
+              placeholder={SUB_STATUS.find(item => item.value == data?.status)?.label}
+              style={{ width: '60%' }}
+              onChange={handleSubChange}
+              options={statusOptions}
+            />
+          ) : (
+            SUB_STATUS.find(item => item.value == data?.status)?.label
+          )}
+        </Form.Item>
+        {renderManage()}
+        {showUpload && (
+          <Form.Item className={styles.formItem} label="上传文件">
+            <Upload {...uploadProps}>
+              <Button icon={<UploadOutlined />}>上传文件</Button>
+            </Upload>
+          </Form.Item>
+        )}
+        <Form.Item className={styles.formItem} label="项目规模">
+          <TableRender onlyShow={true} value={data.process_info} />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={onSave}>
+            保存
+          </Button>
+        </Form.Item>
       </Form>
-    </>
-  );
+    );
+  };
 
   const flow = useMemo(() => {
     if (!data.flow_id) return {};
@@ -114,7 +486,6 @@ function DetailModal(props) {
     <div className={styles.authDetail}>
       <div className={styles.subTitle}>审核详情</div>
       <Steps className={styles.auth} current={current}>
-        {/* <Steps current={data?.node_id}> */}
         {(flow.Nodes || []).map(item => (
           <Step key={item.id} title={item.node} description={`审批人:${getAudits(item)}`} />
         ))}
@@ -124,10 +495,20 @@ function DetailModal(props) {
 
   return (
     <Modal title="项目详情" width={800} visible={visible} onCancel={onClose} footer={null}>
-      {/* {data.type_id != 7 && renderDetail()} */}
       {renderDetail()}
-      {data.audit_status != 0 && renderAuth()}
+      {/* {data.audit_status != 0 && renderAuth()} */}
+      <Tabs defaultActiveKey="1">
+        <Tabs.TabPane tab="成员管理" key="1">
+          <MemberModal isEdit={promise.showMember} currentItem={data} />
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="审核详情" key="2">
+          {renderAuth()}
+        </Tabs.TabPane>
+        <Tabs.TabPane tab="状态记录" key="3">
+          <StatusRender statusList={statusList} />
+        </Tabs.TabPane>
+      </Tabs>
     </Modal>
   );
 }
-export default DetailModal;
+export default connect(({ loading }) => ({ loading: loading.models.approval }))(DetailModal);

+ 1 - 1
src/pages/PurchaseList/Approval/DetailModal.less

@@ -12,5 +12,5 @@
 }
 
 .formItem {
-  margin-bottom: 0;
+  margin-bottom: 11px;
 }

+ 236 - 123
src/pages/PurchaseList/Approval/List.js

@@ -9,8 +9,9 @@ import {
   Popover,
   Input,
   Checkbox,
+  message,
 } from 'antd';
-import moment from 'moment';
+import dayjs from 'dayjs';
 import styles from './List.less';
 import ApprovalModal from './ApprovalModal';
 import DetailModal from './DetailModal';
@@ -20,21 +21,34 @@ import QualityOperateModal from './QualityOperateModal';
 import BudgetModal from './BudgetModal';
 import ModifyManagerModal from './ModifyManagerModal';
 import { connect } from 'dva';
-import { useRequest, useModel } from '@umijs/max';
+import FirmModal from './ManufacturerModal';
+import { queryCreaterList, saveMfr } from '@/services/manufacturer';
+import ProjectRecordModal from './ProjectRecordModal';
+import { useModel } from '@umijs/max';
 
 const { Option } = Select;
-//状态
-const STATUS = [
+//项目阶段
+export const STATUS = [
   { value: 0, label: '售前' },
-  { value: 1, label: '转执行' },
-  { value: 2, label: '转运营' },
-  { value: 3, label: '转质保' },
+  { value: 1, label: '执行' },
+  { value: 2, label: '运营' },
+  { value: 3, label: '质保' },
+];
+//现阶段状态
+export const SUB_STATUS = [
+  { value: 1, label: '初步交流' },
+  { value: 2, label: '预算和方式' },
+  { value: 3, label: '招标' },
+  { value: 4, label: '中标' },
+  { value: 11, label: '执行' },
+  { value: 21, label: '运营' },
+  { value: 31, label: '质保' },
+  { value: 41, label: '失败' },
+  { value: 42, label: '放弃' },
+  { value: 43, label: '关闭' },
 ];
 
 function List(props) {
-  const {
-    initialState: { user },
-  } = useModel('@@initialState');
   const {
     industryList,
     typeList,
@@ -45,7 +59,10 @@ function List(props) {
     depUserTree,
     member,
     budget,
+    supplierList,
   } = props;
+  const { initialState } = useModel('@@initialState');
+  const currentUser = initialState?.user || {};
   const [form] = Form.useForm();
   const [addVisible, setAddVisible] = useState(false);
   const [detailVisible, setDetailVisible] = useState(false);
@@ -57,6 +74,10 @@ function List(props) {
   const [currentItem, setCurrentItem] = useState({});
   const [qualityOperate, setQualityOperate] = useState(0);
   const [modifyManagerVisible, setModifyManagerVisible] = useState(false);
+  const [addFirmVisible, setAddFirmVisible] = useState(false);
+  const [recordVisible, setRecordVisible] = useState(false);
+  const [isEdit, setIsEdit] = useState(false);
+
   const columns = [
     {
       title: '项目编号',
@@ -67,52 +88,63 @@ function List(props) {
       dataIndex: 'project_name',
     },
     {
-      title: '分类',
-      dataIndex: 'TypeInfo',
-      render: (TypeInfo) =>
-        TypeInfo ? `${TypeInfo.name}(${TypeInfo.code})` : '-',
-    },
-    /*
-    {
-      title: '名称',
-      dataIndex: 'name',
+      title: '客户',
+      dataIndex: 'supplier_name',
     },
     {
-      title: '行业',
-      dataIndex: 'IndustryInfo',
-      render: IndustryInfo => `${IndustryInfo.name}(${IndustryInfo.code})`,
+      title: '工艺',
+      dataIndex: 'process_info',
+      render: (info) => {
+        let str = '';
+        if (info) {
+          const data = JSON.parse(info) || [];
+          const list = data.map((item) => item.type);
+          str = list.join('+');
+        }
+        return str;
+      },
     },
+    // {
+    //   title: '规模',
+    //   dataIndex: 'process_info',
+    //   render: info => {
+    //     let str = '';
+    //     if (info) {
+    //       const data = JSON.parse(info) || [];
+    //       console.log('-----------------', data);
+    //       const list = data.map(item => item.scale);
+    //       str = list.join('+');
+    //     }
+    //     return str;
+    //   },
+    // },
+
     {
-      title: '所在地',
-      dataIndex: 'location',
-      render: (location, record) => `${location}(${record.location_code})`,
+      title: '项目种类',
+      dataIndex: 'TypeInfo',
+      render: (TypeInfo) =>
+        TypeInfo ? `${TypeInfo.name}(${TypeInfo.code})` : '-',
     },
     {
-      title: '期数',
-      dataIndex: 'version',
-      render: version => `${version}期`,
+      title: '项目阶段',
+      dataIndex: 'project_status',
+      render: (project_status) =>
+        STATUS.find((item) => item.value == project_status)?.label || '-',
     },
-    */
     {
-      title: '流程',
-      dataIndex: ['FlowInfo', 'name'],
+      title: '现阶段状态',
+      dataIndex: 'status',
+      render: (status) =>
+        SUB_STATUS.find((item) => item.value == status)?.label || '-',
     },
     {
-      title: '状态',
-      dataIndex: 'project_status',
-      render: (project_status) => {
-        // return project_status === 0 ? <>售前</> : <>转执行</>;
-        //若添加其他状态则启用以下switch case:
-        switch (project_status) {
-          case 0:
-            return <>售前</>;
-          case 1:
-            return <>转执行</>;
-          case 2:
-            return <>转运营</>;
-          case 3:
-            return <>转质保</>;
-        }
+      title: '现阶段状态时间(天)',
+      dataIndex: 'current_status_start ',
+      align: 'center',
+      render: (time) => {
+        const date = dayjs(new Date());
+        const daysDiff = date.diff(time, 'days');
+        return daysDiff;
       },
     },
     {
@@ -153,7 +185,7 @@ function List(props) {
     {
       title: '创建时间',
       dataIndex: 'c_time',
-      render: (c_time) => moment(c_time).format('YYYY.MM.DD'),
+      render: (c_time) => dayjs(c_time).format('YYYY.MM.DD'),
     },
     {
       title: '执行经理',
@@ -210,10 +242,10 @@ function List(props) {
         <Form.Item label="项目编号" name="projectCode">
           <Input style={{ width: 200 }} />
         </Form.Item>
-        <Form.Item label="状态" name="projectStatus">
+        <Form.Item label="项目阶段" name="projectStatus">
           <Select
             showSearch
-            style={{ width: 120 }}
+            style={{ width: 160 }}
             filterOption={(input, option) =>
               option.props.children
                 .toLowerCase()
@@ -226,6 +258,22 @@ function List(props) {
             ))}
           </Select>
         </Form.Item>
+        <Form.Item label="现阶段状态" name="projectStatus">
+          <Select
+            showSearch
+            style={{ width: 160 }}
+            filterOption={(input, option) =>
+              option.props.children
+                .toLowerCase()
+                .indexOf(input.toLowerCase()) >= 0
+            }
+          >
+            <Option value={null}>全部</Option>
+            {SUB_STATUS.map((item) => (
+              <Option key={item.value}>{item.label}</Option>
+            ))}
+          </Select>
+        </Form.Item>
         <Form.Item>
           <Button type="primary" loading={loading} onClick={handleSearch}>
             查询
@@ -249,7 +297,10 @@ function List(props) {
         callback: () => setAddVisible(false),
       });
     }
+    // 刷新table
+    queryList({ current: 1 });
   };
+
   const onDelete = (item) => {
     Modal.confirm({
       title: '删除',
@@ -265,6 +316,7 @@ function List(props) {
       },
     });
   };
+
   const onSubmitAuth = (item) => {
     Modal.confirm({
       title: '提交审核',
@@ -283,6 +335,7 @@ function List(props) {
       },
     });
   };
+
   const queryList = (page) => {
     dispatch({
       type: 'approval/queryApproval',
@@ -301,41 +354,54 @@ function List(props) {
       }
     };
     let detailBtn = (
-      <a
-        onClick={() => {
-          setCurrentItem(record);
-          setDetailVisible(true);
-        }}
-      >
-        项目详情
-      </a>
-    );
-    let memberBtn = (
-      <a
-        onClick={() => {
-          setCurrentItem(record);
-          setMemberVisible(true);
-        }}
-      >
-        成员管理
-      </a>
-    );
-    let executionBtn = (
-      <a
-        onClick={() => {
-          setCurrentItem(record);
-          setExecutionVisible(true);
-        }}
-      >
-        转执行
-      </a>
+      <>
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            setRecordVisible(true);
+          }}
+        >
+          项目日志
+        </a>
+        <Divider type="vertical" />
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            setDetailVisible(true);
+            setIsEdit(false);
+          }}
+        >
+          项目详情
+        </a>
+      </>
     );
+    // let memberBtn = (
+    //   <a
+    //     onClick={() => {
+    //       setCurrentItem(record);
+    //       setMemberVisible(true);
+    //     }}
+    //   >
+    //     成员管理
+    //   </a>
+    // );
+    // let executionBtn = (
+    //   <a
+    //     onClick={() => {
+    //       setCurrentItem(record);
+    //       setExecutionVisible(true);
+    //     }}
+    //   >
+    //     转执行
+    //   </a>
+    // );
     let editBtn = (
       <>
         <a
           onClick={() => {
             setCurrentItem(record);
             setAddVisible(true);
+            setIsEdit(true);
           }}
         >
           编辑
@@ -358,6 +424,17 @@ function List(props) {
         </a>
       </>
     );
+    let projectEditBtn = (
+      <a
+        onClick={() => {
+          setIsEdit(true);
+          setCurrentItem(record);
+          setDetailVisible(true);
+        }}
+      >
+        项目编辑
+      </a>
+    );
     let statusBtn = (
       <>
         <a
@@ -376,7 +453,7 @@ function List(props) {
         >
           设置人日预算
         </a>
-        <Divider type="vertical" />
+        {/* <Divider type="vertical" />
         <a
           onClick={() => {
             setCurrentItem(record);
@@ -395,101 +472,105 @@ function List(props) {
           }}
         >
           转运营
-        </a>
+        </a> */}
       </>
     );
     let { audit_status, project_status, type_id } = record;
     //权限审核
-    let canEdit = () => {
-      if (user.IsSuper) return true;
+    let getEditStatus = () => {
+      if (currentUser.IsSuper) return true;
       switch (audit_status) {
         case 0:
-          return user.ID == record.author;
+          return currentUser.ID == record.author;
         case 1:
           return false;
         case 2:
-          if (project_status == 0) return user.ID == record.author;
-          if (project_status == 1) return user.ID == record.LeaderId;
+          if (project_status == 0) return currentUser.ID == record.author;
+          if (project_status == 1) return currentUser.ID == record.LeaderId;
           return false;
         case 3:
           switch (project_status) {
             case 0:
-              return user.ID == record.author;
+              return currentUser.ID == record.author;
             case 1:
-              return user.ID == record.LeaderId;
+              return currentUser.ID == record.LeaderId;
             case 2:
               return (
-                user.ID == record.LeaderId || user.ID == record.opt_manager_id
+                currentUser.ID == record.LeaderId ||
+                currentUser.ID == record.opt_manager_id
               );
             case 3:
               return (
-                user.ID == record.LeaderId || user.ID == record.wty_manager_id
+                currentUser.ID == record.LeaderId ||
+                currentUser.ID == record.wty_manager_id
               );
           }
           return false;
       }
     };
     let toReturn = [];
+    const canEdit = getEditStatus();
     dividerPush(detailBtn, toReturn);
     switch (audit_status) {
       //未提交
       case 0:
-        canEdit() && dividerPush(editBtn, toReturn);
+        canEdit && dividerPush(editBtn, toReturn);
         break;
       //审核中
       case 1:
         break;
       //审核拒绝
       case 2:
-        if (project_status == 0 && canEdit()) dividerPush(editBtn, toReturn);
-        else if (project_status == 1 && canEdit()) {
-          dividerPush(memberBtn, toReturn);
+        if (project_status == 0 && canEdit) dividerPush(editBtn, toReturn);
+        else if (project_status == 1 && canEdit) {
+          // dividerPush(memberBtn, toReturn);
           dividerPush(statusBtn, toReturn);
         }
         break;
       //审核通过
       case 3:
+        canEdit && dividerPush(projectEditBtn, toReturn);
         switch (project_status) {
           //售前
           case 0:
-            if (canEdit()) {
-              dividerPush(memberBtn, toReturn);
-              dividerPush(executionBtn, toReturn);
+            if (canEdit) {
+              // dividerPush(memberBtn, toReturn);
+              // dividerPush(executionBtn, toReturn);
             }
             break;
           //转执行
           case 1:
-            if (canEdit()) {
-              dividerPush(memberBtn, toReturn);
+            if (canEdit) {
+              // dividerPush(memberBtn, toReturn);
               dividerPush(statusBtn, toReturn);
             }
             break;
           //转运营
           case 2:
-            canEdit() && dividerPush(memberBtn, toReturn);
+            // canEdit && dividerPush(memberBtn, toReturn);
             break;
           //转质保
           case 3:
-            canEdit() && dividerPush(memberBtn, toReturn);
+            // canEdit && dividerPush(memberBtn, toReturn);
             break;
         }
         break;
     }
-    let modifyManager = (
-      <a
-        onClick={() => {
-          setCurrentItem(record);
-          setModifyManagerVisible(true);
-        }}
-      >
-        修改项目经理
-      </a>
-    );
-    if (
-      (project_status === 0 || project_status === 1) &&
-      (user.Permission['func-01-point-pm-list-change'] || user.IsSuper)
-    )
-      dividerPush(modifyManager, toReturn);
+    // let modifyManager = (
+    //   <a
+    //     onClick={() => {
+    //       setCurrentItem(record);
+    //       setModifyManagerVisible(true);
+    //     }}
+    //   >
+    //     修改项目经理
+    //   </a>
+    // );
+    // if (
+    //   (project_status === 0 || project_status === 1) &&
+    //   (permission['func-01-point-pm-list-change'] || currentUser.IsSuper)
+    // )
+    //   dividerPush(modifyManager, toReturn);
     return toReturn;
   };
 
@@ -509,8 +590,29 @@ function List(props) {
     dispatch({
       type: 'approval/fetchDepV2',
     });
+    dispatch({
+      type: 'approval/querySupplierList',
+      payload: { project_id: 1, is_super: true, page_size: 999 },
+    });
   }, []);
 
+  const handlerSaveFirm = async (fieldsValue) => {
+    const res = await saveMfr({
+      ...fieldsValue,
+      project_id: 1,
+      created_by: currentUser?.CName,
+    });
+    if (res.code == 200) {
+      message.success('新增成功');
+      setAddFirmVisible(false);
+      dispatch({
+        type: 'approval/querySupplierList',
+        payload: { project_id: 1, is_super: true, page_size: 999 },
+      });
+    }
+    console.log(res);
+  };
+
   return (
     <div>
       {renderSearch()}
@@ -536,7 +638,8 @@ function List(props) {
         onChange={queryList}
       />
       <ApprovalModal
-        currentUser={user}
+        supplierList={supplierList}
+        currentUser={currentUser}
         depUserTree={depUserTree}
         loading={loading}
         industryList={industryList}
@@ -547,13 +650,17 @@ function List(props) {
         data={currentItem}
         total={data.pagination.total}
         onClose={() => setAddVisible(false)}
+        onAddFirm={() => setAddFirmVisible(true)}
       />
       <DetailModal
+        depUserTree={depUserTree}
         industryList={industryList}
         flowList={flowList}
         typeList={typeList}
         visible={detailVisible}
         data={currentItem}
+        isEdit={isEdit}
+        currentUser={currentUser}
         onClose={() => setDetailVisible(false)}
       />
       <ExecutionModal
@@ -564,14 +671,6 @@ function List(props) {
         onOk={() => setExecutionVisible(false)}
         onClose={() => setExecutionVisible(false)}
       />
-      <MemberModal
-        depUserTree={depUserTree}
-        loading={loading}
-        visible={memberVisible}
-        onClose={() => setMemberVisible(false)}
-        currentItem={currentItem}
-        dataSource={member}
-      />
       <QualityOperateModal
         depUserTree={depUserTree}
         loading={loading}
@@ -598,6 +697,17 @@ function List(props) {
         dataSource={member}
         onOk={() => setModifyManagerVisible(false)}
       />
+      <FirmModal
+        visible={addFirmVisible}
+        onCancel={() => setAddFirmVisible(false)}
+        onOk={handlerSaveFirm}
+      />
+      <ProjectRecordModal
+        depUserTree={depUserTree}
+        currentItem={currentItem}
+        visible={recordVisible}
+        onClose={() => setRecordVisible(false)}
+      />
     </div>
   );
 }
@@ -607,8 +717,11 @@ export default connect(({ approval, user, loading }) => ({
   typeList: approval.typeList,
   flowList: approval.flowList,
   industryList: approval.industryList,
+  currentUser: user.currentUser,
+  // permission: user.currentUser.Permission,
   loading: loading.models.approval,
   depUserTree: approval.depUserTree,
   member: approval.member,
   budget: approval.budget,
+  supplierList: approval.supplierList,
 }))(List);

+ 330 - 0
src/pages/PurchaseList/Approval/ManufacturerModal.js

@@ -0,0 +1,330 @@
+import React, { Fragment, useState, useEffect, useRef, useMemo } from 'react';
+import {
+  Table,
+  Icon,
+  message,
+  Spin,
+  Button,
+  Form,
+  DatePicker,
+  Row,
+  Col,
+  Modal,
+  Input,
+  Upload,
+  Select,
+} from 'antd';
+import dayjs from 'dayjs';
+import { getToken, GetTokenFromUrl } from '@/utils/utils';
+const { RangePicker } = DatePicker;
+const { TextArea } = Input;
+function FirmModal(props) {
+  const {
+    onCancel,
+    onOk,
+    visible,
+    item = null,
+    disabled = true,
+    form,
+    typeDisabled = false,
+  } = props;
+  const [formRef] = Form.useForm();
+  const [type, setType] = useState(item?.type || 1);
+  // console.log(type);
+  const handleOnOk = async () => {
+    formRef
+      .validateFields()
+      .then(values => {
+        console.log(values);
+        formRef.resetFields();
+        onOk?.({ ...values });
+      })
+      .catch(info => {
+        console.log('Validate Failed:', info);
+      });
+
+    // ((err, fieldsValue) => {
+    //   if (err)
+    //     return;
+    //   const values = {
+    //     ...fieldsValue,
+    //   };
+    //   onOk?.(values);
+    // });
+  };
+  const handleOnCancel = () => {
+    formRef.resetFields();
+    onCancel?.();
+  };
+  const handleType = value => {
+    formRef.resetFields();
+    formRef.setFieldValue('type', value);
+    setType(value);
+  };
+  const token = getToken() || GetTokenFromUrl();
+  const uploadProps = {
+    name: 'files',
+    // action: `/api/v1/device-color/upload/${ID}`,
+    headers: {
+      'JWT-TOKEN': token,
+    },
+    showUploadList: false,
+    onChange: info => {
+      if (info.file.status !== 'uploading') {
+        console.log(info.file, info.fileList);
+      }
+      if (info.file.status === 'done') {
+        message.success(`${info.file.name} 文件上传成功`);
+      } else if (info.file.status === 'error') {
+        message.error(`${info.file.name} 文件上传失败`);
+      }
+    },
+  };
+  useEffect(() => {
+    formRef.resetFields();
+    if (visible) setType(item?.type || 1);
+  }, [visible]);
+
+  return (
+    <Modal
+      maskClosable={false}
+      open={visible}
+      destroyOnClose
+      onCancel={handleOnCancel}
+      onOk={handleOnOk}
+      // forceRender
+      width="60%"
+      title="新增"
+      // footer={disabled ? null : undefined}
+    >
+      <div style={{ padding: 30 }}>
+        <Form
+          form={formRef}
+          // layout="inline"
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          preserve={false}
+          initialValues={{
+            name: item?.name || '',
+            tax_code: item?.tax_code || '',
+            contact: item?.contact || '',
+            phone_number: item?.phone_number || '',
+            bank_account: item?.bank_account || '',
+            bank_number: item?.bank_number || '',
+            address: item?.address || '',
+            type: item?.type || 1,
+            id_type: item?.id_type || '',
+            id_card: item?.id_card || '',
+          }}
+        >
+          <Row gutter={[48, 24]}>
+            <Col span={12}>
+              <Form.Item
+                style={{ width: '100%' }}
+                label="主体类型:"
+                name="type"
+                rules={[
+                  {
+                    required: true,
+                    message: '请选择主体类型',
+                  },
+                ]}
+              >
+                <Select
+                  options={[
+                    { value: 1, label: '供应商' },
+                    { value: 4, label: '自然人' },
+                  ]}
+                  placeholder="请选择主体类型"
+                  onChange={handleType}
+                ></Select>
+              </Form.Item>
+            </Col>
+          </Row>
+          {type === 1 && (
+            <>
+              <Row gutter={[48, 24]}>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="名称:"
+                    name="name"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入供应商名称',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入供应商名称" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="税号:"
+                    name="tax_code"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入税号',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入税号" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row gutter={[48, 24]}>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="联系人:"
+                    name="contact"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入联系人',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入联系人" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="联系电话:"
+                    name="phone_number"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入联系电话',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入联系电话" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row gutter={[48, 24]}>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="开户银行:"
+                    name="bank_account"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入开户银行',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入开户银行" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="银行账号:"
+                    name="bank_number"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入银行账号',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入银行账号" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row>
+                <Col span={24}>
+                  <Form.Item
+                    labelCol={{ span: 2 }}
+                    wrapperCol={{ span: 22 }}
+                    style={{ width: '100%' }}
+                    label="地址:"
+                    name="address"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入地址',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入地址" />
+                  </Form.Item>
+                </Col>
+              </Row>
+            </>
+          )}
+          {type === 4 && (
+            <>
+              <Row gutter={[48, 24]}>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="姓名:"
+                    name="name"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入姓名',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入姓名" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="证件类型:"
+                    name="id_type"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请选择证件类型',
+                      },
+                    ]}
+                  >
+                    <Select placeholder="请选择证件类型">
+                      <Option value={1}>身份证</Option>
+                      <Option value={2}>护照</Option>
+                    </Select>
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row gutter={[48, 24]}>
+                <Col span={12}>
+                  <Form.Item
+                    style={{ width: '100%' }}
+                    label="证件号:"
+                    name="id_card"
+                    rules={[
+                      {
+                        required: true,
+                        message: '请输入证件号',
+                      },
+                    ]}
+                  >
+                    <Input placeholder="请输入证件号" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item style={{ width: '100%' }} label="银行账号:" name="bank_number">
+                    <Input placeholder="请输入银行账号" />
+                  </Form.Item>
+                </Col>
+              </Row>
+            </>
+          )}
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+export default FirmModal;

+ 54 - 50
src/pages/PurchaseList/Approval/MemberModal.js

@@ -8,12 +8,22 @@ const { TabPane } = Tabs;
 const STATUS = [
   { value: 1, label: '售前' },
   { value: 2, label: '执行' },
-  { value: 4, label: '运营' },
+  { value: 4, label: '研发' },
+  { value: 5, label: '运营' },
   { value: 6, label: '质保' },
 ];
 
-function MemberModal(props) {
-  const { visible, onClose, currentItem, loading, depUserTree, dataSource, dispatch } = props;
+function MemberRender(props) {
+  const {
+    visible,
+    onClose,
+    isEdit = false,
+    currentItem,
+    loading,
+    depUserTree,
+    member: dataSource,
+    dispatch,
+  } = props;
   const [type, setType] = useState('1');
   const [form] = Form.useForm();
   const [currentMember, setCurrentMember] = useState({});
@@ -90,20 +100,7 @@ function MemberModal(props) {
   }, [currentItem]);
 
   return (
-    <Modal
-      title="成员管理"
-      confirmLoading={loading}
-      maskClosable={false}
-      destroyOnClose
-      visible={visible}
-      onCancel={() => {
-        form.resetFields();
-        setType('1');
-        onClose();
-      }}
-      footer={null}
-      width="70%"
-    >
+    <>
       <Tabs defaultActiveKey="1" activeKey={type} onChange={type => handleChange(type)}>
         {STATUS.map(
           item =>
@@ -112,41 +109,48 @@ function MemberModal(props) {
             )
         )}
       </Tabs>
-      <Form
-        labelCol={{ span: 6 }}
-        wrapperCol={{ span: 10 }}
-        width="100%"
-        style={{ marginBottom: 20 }}
-        layout="inline"
-        form={form}
-      >
-        <Form.Item
-          label="添加成员"
-          name="memberID"
-          initialValue={null}
-          rules={[{ required: true, message: '请选择成员' }]}
+      {isEdit && (
+        <Form
+          labelCol={{ span: 6 }}
+          wrapperCol={{ span: 10 }}
+          width="100%"
+          style={{ marginBottom: 20 }}
+          layout="inline"
+          form={form}
         >
-          <TreeSelect
-            showSearch
-            allowClear
-            style={{ width: 240 }}
-            placeholder="请选择项目成员"
-            multiple={false}
-            treeData={depUserTree}
-            filterTreeNode={(input, option) => {
-              return option.props.title === input;
-            }}
-          />
-        </Form.Item>
-        <Form.Item>
-          <Button type="primary" loading={loading} onClick={handleAddMember}>
-            添加
-          </Button>
-        </Form.Item>
-      </Form>
+          <Form.Item
+            label="添加成员"
+            name="memberID"
+            initialValue={null}
+            rules={[{ required: true, message: '请选择成员' }]}
+          >
+            <TreeSelect
+              showSearch
+              allowClear
+              style={{ width: 240 }}
+              placeholder="请选择项目成员"
+              multiple={false}
+              treeData={depUserTree}
+              filterTreeNode={(input, option) => {
+                return option.props.title === input;
+              }}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" loading={loading} onClick={handleAddMember}>
+              添加
+            </Button>
+          </Form.Item>
+        </Form>
+      )}
       <Table columns={columns} loading={loading} dataSource={dataSource} pagination={false} />
-    </Modal>
+    </>
   );
 }
 
-export default connect()(MemberModal);
+export default connect(({ approval, user, loading }) => ({
+  currentUser: user.currentUser,
+  loading: loading.models.approval,
+  depUserTree: approval.depUserTree,
+  member: approval.member,
+}))(MemberRender);

+ 123 - 0
src/pages/PurchaseList/Approval/ProjectRecordModal.js

@@ -0,0 +1,123 @@
+import { Button, DatePicker, Form, Modal, Table, TreeSelect } from 'antd';
+import styles from './index.less';
+import { useRequest } from '@umijs/max';
+import { approvalLogProjectsList } from '@/services/record';
+import { useEffect, useState } from 'react';
+import dayjs from 'dayjs';
+const { RangePicker } = DatePicker;
+const ProjectRecordModal = ({
+  visible,
+  currentItem,
+  depUserTree,
+  onOk,
+  onClose,
+}) => {
+  const [searchData, setSearchData] = useState({
+    author: '',
+    s_time: '',
+    e_time: '',
+    currentPage: 1,
+  });
+  const { data, run, loading } = useRequest(
+    (data) => approvalLogProjectsList({ ...data, id: currentItem.id }),
+    {
+      manual: true,
+    },
+  );
+  useEffect(() => {
+    if (visible && currentItem?.id) run({});
+  }, [visible, currentItem]);
+  const columns = [
+    {
+      title: '提交人',
+      dataIndex: 'author_name',
+      width: '12%',
+    },
+    {
+      title: '日志标题',
+      dataIndex: 'title',
+      width: '20%',
+    },
+    {
+      title: '日志概述',
+      dataIndex: 'content',
+      render: (doc) => <div className={styles.doc}>{doc}</div>,
+    },
+    {
+      title: '提交时间',
+      dataIndex: 'c_time',
+      width: '20%',
+      render: (time) => dayjs(time).format('YYYY-MM-DD HH:mm'),
+    },
+    // {
+    //   title: '操作',
+    //   width: '12%',
+    //   render: () => <a>详情</a>,
+    // },
+  ];
+
+  const onAuthorChange = (ids) => {
+    const [user_id, dep_id] = ids.split('||');
+    setSearchData({ ...searchData, author: user_id });
+  };
+
+  const onDateChange = (date) => {
+    const s_time = dayjs(date[0]).format('YYYY-MM-DD');
+    const e_time = dayjs(date[1]).format('YYYY-MM-DD');
+    setSearchData({ ...searchData, s_time, e_time });
+  };
+
+  const handleTableChange = (pagination) => {
+    setSearchData({ ...searchData, pageSize: pagination.current });
+  };
+
+  return (
+    <Modal
+      title="日志详情"
+      open={visible}
+      width={1200}
+      onOk={onOk}
+      onCancel={onClose}
+      footer={null}
+    >
+      <Form
+        labelCol={{ span: 6 }}
+        wrapperCol={{ span: 18 }}
+        width="100%"
+        layout="inline"
+        style={{ marginBottom: 20 }}
+      >
+        <Form.Item label="提交人:">
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: 240 }}
+            placeholder="请选择提交人"
+            multiple={false}
+            onChange={onAuthorChange}
+            treeData={depUserTree}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+          />
+        </Form.Item>
+        <Form.Item label="时间:">
+          <RangePicker style={{ width: 240 }} onChange={onDateChange} />
+        </Form.Item>
+        <Form.Item wrapperCol={{ span: 10, offset: 14 }}>
+          <Button type="primary" onClick={() => run(searchData)}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+      <Table
+        loading={loading}
+        columns={columns}
+        dataSource={data?.list}
+        onChange={handleTableChange}
+        pagination={data?.pagination}
+      />
+    </Modal>
+  );
+};
+export default ProjectRecordModal;

+ 131 - 0
src/pages/PurchaseList/Approval/Statistic.js

@@ -0,0 +1,131 @@
+import { useEffect, useMemo, useRef } from 'react';
+import styles from './index.less';
+import echarts from 'echarts';
+import BarChartModule from '@/components/charts/BarChartModule';
+import { Radio } from 'antd';
+import { useRequest } from '@umijs/max';
+import {
+  workloadDepProjectChart,
+  workloadDepProjectTypeChart,
+} from '@/services/record';
+import PieChartModule from '@/components/charts/PieChartModule';
+import { SUB_STATUS } from './List';
+import { connect } from 'dva';
+const Statistic = (props) => {
+  const { typeList, dispatch } = props;
+  const { data, loading } = useRequest(workloadDepProjectChart);
+  const {
+    data: typeData,
+    run: runTypeData,
+    loading: typeLoading,
+  } = useRequest(workloadDepProjectTypeChart); //1  月 2 季度 3 年
+  useEffect(() => {
+    dispatch({
+      type: 'approval/queryType',
+    });
+  }, []);
+
+  const titleData = useMemo(() => {
+    return [
+      {
+        name: '项目总数',
+        count: data?.total,
+      },
+      {
+        name: '审核通过项目数',
+        count: data?.pass,
+      },
+      {
+        name: '审核中项目数',
+        count: data?.audit,
+      },
+      {
+        name: '本月新增立项项目',
+        count: data?.add,
+      },
+    ];
+  }, [data]);
+
+  const pieData = useMemo(() => {
+    return data?.status_chart?.map((item) => {
+      const name =
+        SUB_STATUS.find((cur) => cur.value == item.status)?.label || '';
+      return { value: item.num, name };
+    });
+  }, [data]);
+
+  const barData = useMemo(() => {
+    if (!typeData || typeData?.length == 0) return;
+    const newData = {};
+    newData.xData = typeData.map((item) => item.create_time);
+    const types = [...new Set(typeData.map((item) => item.type))];
+    newData.dataList = types.map((type) => {
+      const name = typeList?.find((cur) => cur.id == type)?.name;
+      const data = typeData.filter((item) => item.type == type);
+      return { name, data: data?.map((item) => item.num) };
+    });
+    return newData;
+  }, [typeData]);
+  console.log(barData);
+
+  const options = [
+    {
+      label: '月',
+      value: 1,
+    },
+    {
+      label: '季度',
+      value: 2,
+    },
+    {
+      label: '年',
+      value: 3,
+    },
+  ];
+  return (
+    <div className={styles.statistic}>
+      <div className={styles.boxCon}>
+        <div className={styles.titleText}>项目统计</div>
+        <div className={styles.titleCon}>
+          {titleData.map((item) => (
+            <div className={styles.item}>
+              <div className={styles.itemNum}>{item.count}</div>
+              <div>{item.name}</div>
+            </div>
+          ))}
+        </div>
+      </div>
+      <div className={styles.bottomCon}>
+        <div className={styles.boxCon} style={{ width: '49.2%' }}>
+          <div className={styles.titleText}>项目状态统计</div>
+          <div className={styles.chartCon}>
+            {data?.status_chart?.length > 0 && (
+              <PieChartModule data={pieData} />
+            )}
+          </div>
+        </div>
+        <div
+          className={styles.boxCon}
+          style={{ width: '49.2%', position: 'relative' }}
+        >
+          <div className={styles.titleText}>项目分类统计</div>
+          <div className={styles.pieContent}>
+            <Radio.Group
+              options={options}
+              onChange={(e) => runTypeData({ t: e.target.value })}
+              defaultValue={1}
+              optionType="button"
+              buttonStyle="solid"
+            />
+          </div>
+          <div className={styles.chartCon}>
+            {typeData && <BarChartModule {...barData} />}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+export default connect(({ approval, user, loading }) => ({
+  typeList: approval.typeList,
+}))(Statistic);

+ 54 - 0
src/pages/PurchaseList/Approval/StatusRender.js

@@ -0,0 +1,54 @@
+import { Timeline } from 'antd';
+import styles from './index.less';
+import { STATUS, SUB_STATUS } from './List';
+import { useMemo } from 'react';
+import dayjs from 'dayjs';
+
+const StatusRender = ({ statusList }) => {
+  const list = useMemo(() => {
+    return statusList.map((item) => {
+      let attach = [];
+      if (item.attach) {
+        try {
+          attach = JSON.parse(item.attach);
+        } catch (e) {
+          console.error(e);
+        }
+        attach = attach
+          .map((item) => {
+            if (item instanceof Object) {
+              return (
+                <div>
+                  <a target="_blank" download={item.name} href={item.url}>
+                    {item.name}
+                  </a>
+                </div>
+              );
+            }
+          })
+          .filter((item) => item);
+      }
+      return { ...item, attach };
+    });
+  });
+  return (
+    <Timeline>
+      {list?.map((item, index) => (
+        <Timeline.Item dot={<div className={styles.icon}>{index + 1}</div>}>
+          <div style={{ fontSize: '16px', color: '#1890ff' }}>
+            {STATUS.find((cur) => cur.value == item.project_status)?.label}-
+            {SUB_STATUS.find((cur) => cur.value == item.status)?.label}
+          </div>
+          <div>
+            持续时间:{item.continuously}
+            &nbsp;&nbsp;&nbsp;&nbsp; 修改人员:{item.author_name}
+            &nbsp;&nbsp;&nbsp;&nbsp; 修改时间:
+            {dayjs(item.c_time).format('YYYY-MM-DD')}
+          </div>
+          {item.attach.length > 0 && <div>附件: {item.attach}</div>}
+        </Timeline.Item>
+      ))}
+    </Timeline>
+  );
+};
+export default StatusRender;

+ 89 - 0
src/pages/PurchaseList/Approval/TableRender.js

@@ -0,0 +1,89 @@
+import { useMemo, useState } from 'react';
+import styles from './index.less';
+import { Input, Select } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+const TableRender = ({ value, onChange, onlyShow = false }) => {
+  const defaultItem = { type: '', scale: '' };
+  const options = ['UF', 'RO', 'NF', 'MF', 'MBR'];
+
+  const list = useMemo(() => {
+    return value ? JSON.parse(value) : [defaultItem];
+  }, [value]);
+
+  const handleChange = (idx, item) => {
+    const newList = JSON.parse(JSON.stringify(list));
+    newList[idx] = item;
+    const result = JSON.stringify(newList);
+    onChange(result);
+  };
+
+  const handleAddClick = () => {
+    const result = JSON.stringify([...list, defaultItem]);
+    onChange(result);
+  };
+
+  const handleDelClick = idx => {
+    const newList = JSON.parse(JSON.stringify(list));
+    newList.splice(idx, 1);
+    const result = JSON.stringify(newList);
+    onChange(result);
+  };
+
+  const renderItems = (item, idx) => {
+    const { type, scale } = item;
+    return (
+      <div className={styles.item} key={`${idx}`}>
+        <Select
+          value={type}
+          className={styles.itemLeft}
+          style={{ width: '100%' }}
+          onChange={e => handleChange(idx, { ...item, type: e })}
+        >
+          {options.map(value => (
+            <Option key={value}>{value}</Option>
+          ))}
+        </Select>
+        <Input
+          value={scale}
+          onChange={e => handleChange(idx, { ...item, scale: e.target.value })}
+        />
+        <DeleteOutlined className={styles.deleteIcon} onClick={() => handleDelClick(idx)} />
+      </div>
+    );
+  };
+  const renderOnlyShowItems = (item, idx) => {
+    const { type, scale } = item;
+    return (
+      <div className={styles.item} key={`${type}_${scale}_${idx}`}>
+        <span>{type}</span>
+        <div className={styles.line}></div>
+        <span>{scale}</span>
+      </div>
+    );
+  };
+  return (
+    <div>
+      {onlyShow ? (
+        <div className={styles.content} style={{ width: '60%' }}>
+          {list.map((item, idx) => renderOnlyShowItems(item, idx))}
+        </div>
+      ) : (
+        <>
+          <div className={styles.titleContent}>
+            <div>工艺技术</div>
+            <div>项目规模</div>
+          </div>
+
+          <div className={styles.content}>
+            {list.map((item, idx) => renderItems(item, idx))}
+
+            <div className={styles.addBtn} onClick={handleAddClick}>
+              + 添加
+            </div>
+          </div>
+        </>
+      )}
+    </div>
+  );
+};
+export default TableRender;

+ 105 - 0
src/pages/PurchaseList/Approval/index.less

@@ -0,0 +1,105 @@
+.titleContent {
+  background-color: #d5f1ff;
+  height: 32px;
+  //   font-size: 18px;
+  display: flex;
+  justify-content: space-around;
+  border-radius: 2px;
+  align-items: center;
+}
+.content {
+  border: 1px solid #eee;
+  .item {
+    position: relative;
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    margin: 4px 8px;
+    padding-bottom: 4px;
+    border-bottom: 1px solid #eee;
+    .itemLeft {
+      border-right: 1px solid #eee;
+    }
+  }
+  .addBtn {
+    height: 32px;
+    color: #93cafe;
+    text-align: center;
+    line-height: 32px;
+  }
+  .deleteIcon {
+    position: absolute;
+    right: -32px;
+  }
+  .line {
+    height: 30px;
+    width: 1px;
+    background-color: #eee;
+  }
+  :global {
+    .ant-input {
+      border: none;
+    }
+    .ant-select:not(.ant-select-customize-input) .ant-select-selector {
+      border: none;
+    }
+  }
+}
+
+.item:last-child {
+  border-bottom: none;
+}
+.icon {
+  background-color: #1890ff;
+  color: #fff;
+  width: 24px;
+  height: 24px;
+  text-align: center;
+  line-height: 24px;
+  border-radius: 12px;
+}
+.doc {
+  width: 400px;
+  white-space: nowrap; /* 防止文本换行 */
+  overflow: hidden; /* 隐藏溢出的文本 */
+  text-overflow: ellipsis; /* 显示省略号 */
+}
+.statistic {
+  padding: 40px;
+  .boxCon {
+    width: 100%;
+    padding: 10px;
+    border-radius: 12px;
+    box-shadow: 0px 0px 10px 4px rgba(0, 0, 0, 0.2);
+    .titleCon {
+      display: flex;
+      width: 100%;
+      justify-content: space-around;
+      margin: 20px 0;
+      .item {
+        text-align: center;
+      }
+      .itemNum {
+        color: #f5a41f;
+        font-size: 32px;
+      }
+    }
+  }
+  .titleText {
+    font-size: 22px;
+  }
+  .bottomCon {
+    display: flex;
+    margin-top: 26px;
+    justify-content: space-between;
+  }
+  .pieContent {
+    position: absolute;
+    top: 16px;
+    right: 20px;
+  }
+
+  .chartCon {
+    height: 400px;
+  }
+}

+ 13 - 1
src/pages/PurchaseList/Approval/models/approval.js

@@ -20,8 +20,9 @@ import {
   setBudget,
   modifyManager,
 } from '@/services/approval';
+import { queryMfrList } from '@/services/manufacturer';
 import { message } from 'antd';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 function getDepUserTree(data) {
   data.title = `${data.Name}`;
@@ -67,6 +68,7 @@ export default {
     depUserTree: [],
     member: [],
     budget: [],
+    supplierList: [],
   },
 
   effects: {
@@ -278,6 +280,16 @@ export default {
         type: 'queryApproval',
       });
     },
+    *querySupplierList({ payload, callback }, { call, put }) {
+      const res = yield call(queryMfrList, payload);
+      if (!res.data) return;
+      console.log(res);
+      yield put({
+        type: 'save',
+        payload: { supplierList: res.data.list },
+      });
+      callback?.();
+    },
   },
 
   reducers: {

+ 57 - 0
src/pages/PurchaseList/DailyRecord/components/RecordDetail.js

@@ -0,0 +1,57 @@
+import { Button, Col, Form, Input, Modal, Row, Select, Space } from 'antd';
+import styles from './index.less';
+import { useEffect, useMemo, useState } from 'react';
+import { DeleteOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+
+const RecordDetailModal = ({
+  detail,
+  visible,
+  user,
+  projects,
+  loading = false,
+  onOk,
+  onCancel,
+}) => {
+  const data = useMemo(() => {
+    if (detail?.length > 0) {
+      const data = { name: '', time: '' };
+      data.name = detail[0].author_name || '';
+      data.time = detail[0].c_time
+        ? dayjs(detail[0].c_time).format('YYYY-MM-DD HH-mm')
+        : '';
+      return data;
+    }
+  }, [detail]);
+
+  const RenderItem = (item) => {
+    return (
+      <Space key={item.id} direction="vertical" className={styles.itemCon}>
+        <div>项目名称:{item.project_name}</div>
+        <div>日志概述:{item.title}</div>
+        <div>日志详情:{item.content}</div>
+      </Space>
+    );
+  };
+
+  return (
+    <Modal
+      title="日志详情"
+      open={visible}
+      width={800}
+      onOk={onOk}
+      onCancel={onCancel}
+      footer={null}
+    >
+      <div className={styles.titleContent}>
+        <span className={styles.title}>{data?.name}的金科环境项目日志</span>
+        <span>{data?.time}</span>
+      </div>
+      <Space direction="vertical" size={16} className={styles.detailContent}>
+        {detail?.map((item) => RenderItem(item))}
+      </Space>
+    </Modal>
+  );
+};
+
+export default RecordDetailModal;

+ 185 - 0
src/pages/PurchaseList/DailyRecord/components/WriteRecordModal.js

@@ -0,0 +1,185 @@
+import { Button, Col, Form, Input, Modal, Row, Select, message } from 'antd';
+import styles from './index.less';
+import { useEffect, useMemo, useState } from 'react';
+import { DeleteOutlined } from '@ant-design/icons';
+import { approvalLog } from '@/services/record';
+
+const WriteRecordModal = ({ detail, visible, user, projects, loading = false, onOk, onCancel }) => {
+  const [form] = Form.useForm();
+  const defaultData = { id: '', code_id: '', title: '', content: '' };
+
+  const [list, setList] = useState([defaultData]);
+  const [name, setName] = useState();
+  // console.log(user, list);
+
+  useEffect(() => {
+    if (detail?.length > 0) {
+      setList(detail);
+      setName(detail[0].author_name);
+    } else {
+      setList([defaultData]);
+      setName(user.CName);
+    }
+  }, [detail, user]);
+
+  const handleAddClick = () => {
+    setList([...list, defaultData]);
+  };
+
+  const handleChangeItem = (idx, item) => {
+    const newList = [...list];
+    newList[idx] = item;
+    setList(newList);
+  };
+
+  const handleDelItem = idx => {
+    if (list.length <= 1) return;
+    const newList = [...list];
+    newList.splice(idx, 1);
+    setList(newList);
+  };
+
+  const handleOk = () => {
+    // 新建的不需要传id字段
+    const params = list.map(item => {
+      if (item.id === '') {
+        return {
+          code_id: item.code_id,
+          title: item.title,
+          content: item.content,
+        };
+      } else {
+        return item;
+      }
+    });
+    onOk(params);
+  };
+
+  return (
+    <Modal
+      title="写日志"
+      open={visible}
+      width={800}
+      onOk={handleOk}
+      onCancel={onCancel}
+      destroyOnClose
+    >
+      <Form
+        labelCol={{ span: 4 }}
+        wrapperCol={{ span: 18 }}
+        labelAlign="left"
+        width="100%"
+        style={{ marginBottom: 20 }}
+        form={form}
+      >
+        <Form.Item
+          label="日志标题"
+          name="memberID"
+          initialValue={`${name}的金科环境项目日志`}
+          rules={[{ required: true, message: '请选择成员' }]}
+        >
+          <Input />
+        </Form.Item>
+        <Form.Item
+          label="日志详情"
+          name="memberID"
+          initialValue={'管理员的金科环境项目日志'}
+          rules={[{ required: true, message: '请选择成员' }]}
+        >
+          {list.map((item, idx) => (
+            <RenderItem
+              key={`${item.code_id}_${idx}`}
+              idx={idx}
+              projects={projects}
+              data={item}
+              showDeleteIcon={idx !== 0}
+              onChange={handleChangeItem}
+              onDelete={handleDelItem}
+            />
+          ))}
+        </Form.Item>
+
+        <Form.Item wrapperCol={{ span: 20, offset: 4 }}>
+          <Button style={{ width: '100px' }} onClick={handleAddClick}>
+            + 新增
+          </Button>
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+};
+
+export default WriteRecordModal;
+
+const RenderItem = ({ idx, data, showDeleteIcon, projects, onChange, onDelete }) => {
+  const [form] = Form.useForm();
+
+  const projectName = useMemo(() => {
+    return projects.find(item => item.id == data.code_id)?.project_name || '';
+  }, [data.code_id]);
+
+  return (
+    <div className={styles.itemContent}>
+      <Form
+        labelCol={{ span: 8 }}
+        wrapperCol={{ span: 16 }}
+        labelAlign="left"
+        width="100%"
+        form={form}
+      >
+        <Row>
+          <Col span={12}>
+            <Form.Item
+              label="项目名称"
+              name="projectName"
+              initialValue={projectName}
+              rules={[{ required: true, message: '请选择项目' }]}
+            >
+              <Select
+                // style={{ width: 120 }}
+                options={projects?.map(item => {
+                  return { label: item.project_name, value: item.id };
+                })}
+                onChange={id => onChange(idx, { ...data, code_id: id })}
+                filterOption={(input, option) => {
+                  return option.label.includes(input);
+                }}
+                showSearch
+              />
+            </Form.Item>
+          </Col>
+          <Col span={11} offset={1}>
+            <Form.Item
+              label="日志概述"
+              name="title"
+              initialValue={data.title}
+              rules={[{ required: true, message: '请选择成员' }]}
+            >
+              <Input
+                value={data.title}
+                onChange={e => onChange(idx, { ...data, title: e.target.value })}
+              />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Form.Item
+          style={{ marginBottom: 0 }}
+          labelCol={{ span: 4 }}
+          wrapperCol={{ span: 20 }}
+          label="日志详情"
+          initialValue={data.content}
+          name="content"
+        >
+          <Input.TextArea
+            style={{ height: 120 }}
+            value={data.content}
+            onChange={e => onChange(idx, { ...data, content: e.target.value })}
+          />
+        </Form.Item>
+      </Form>
+      {showDeleteIcon && (
+        <DeleteOutlined className={styles.delIcon} onClick={idx => onDelete(idx)} />
+      )}
+    </div>
+  );
+};

+ 40 - 0
src/pages/PurchaseList/DailyRecord/components/index.less

@@ -0,0 +1,40 @@
+.itemContent {
+  position: relative;
+  border: 1px dashed #eee;
+  padding: 10px;
+  margin-bottom: 16px;
+  :global {
+    .ant-col-3 {
+      flex: 0 0 14.5%;
+      max-width: 14.5%;
+    }
+  }
+  .delIcon {
+    position: absolute;
+    top: calc(50% - 15px);
+    right: -30px;
+    font-size: 20px;
+  }
+}
+.itemContent:last-child {
+  margin-bottom: none;
+}
+
+.titleContent {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 24px;
+  .title {
+    font-size: 24px;
+    color: #599ae4;
+  }
+}
+.detailContent {
+  width: 100%;
+  .itemCon {
+    width: 100%;
+    padding: 10px;
+    border-radius: 8px;
+    box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.3);
+  }
+}

+ 273 - 0
src/pages/PurchaseList/DailyRecord/index.js

@@ -0,0 +1,273 @@
+import { Button, DatePicker, Space, Table, Modal, message, Spin } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { useEffect, useMemo, useState } from 'react';
+import WriteRecordModal from './components/WriteRecordModal';
+import { connect } from 'dva';
+import RecordDetailModal from './components/RecordDetail';
+import styles from './index.less';
+import { useRequest } from '@umijs/max';
+import {
+  approvalAddLog,
+  approvalDeleteLog,
+  approvalEditLog,
+  approvalLogDetail,
+  approvalLogOwnList,
+} from '@/services/record';
+import record from './models/record';
+import { log } from 'lodash-decorators/utils';
+import dayjs from 'dayjs';
+import { async } from '@antv/x6/lib/registry/marker/async';
+
+const { confirm } = Modal;
+
+const DailyRecord = (props) => {
+  const { currentUser, projects, loading: propsLoading, dispatch } = props;
+
+  const [queryListParams, setQueryListParams] = useState({
+    current: 1,
+    pageSize: 9,
+  });
+  const [pageInfo, setPageInfo] = useState({
+    current: 1,
+    pageSize: 9,
+    total: 999,
+  });
+  const [writeVisible, setWriteVisible] = useState(false);
+  const [detailVisible, setDetailVisible] = useState(false);
+  const [selectedLogID, setselectedLogID] = useState('');
+  const [logDetail, setLogDetail] = useState([]);
+
+  const [mockLogDetail, setMockLogDetail] = useState([]);
+
+  // 请求列表
+  const {
+    data,
+    run,
+    loading: listLoading,
+  } = useRequest(approvalLogOwnList, {
+    defaultParams: [{ ...queryListParams }],
+    onSuccess: (data) => {
+      if (data?.pagination) {
+        setPageInfo(data.pagination);
+      }
+    },
+  });
+
+  // 查询日志详情
+  // const { data: logDetail, run: runDetail, loading: detailLoading } = useRequest(
+  //   logID => approvalLogDetail(logID),
+  //   {
+  //     manual: true,
+  //     onSuccess: () => {
+  //       setDetailVisible(true);
+  //     },
+  //   }
+  // );
+  // console.log('-----------', logDetail);
+
+  // 添加日志
+  const { run: runAdd, loading: addLoading } = useRequest(approvalAddLog, {
+    manual: true,
+    onSuccess: () => {
+      message.success('添加日志成功');
+      setWriteVisible(false);
+      run(queryListParams);
+    },
+  });
+
+  // 删除日志
+  const { run: runDelete, loading: deleteLoading } = useRequest(
+    approvalDeleteLog,
+    {
+      manual: true,
+      onSuccess: () => {
+        message.success('删除日志成功');
+        run({ currentPage: 1, pageSize: 9 });
+      },
+    },
+  );
+
+  // 编辑日志
+  const { run: runEdit, loading: editLoading } = useRequest(approvalEditLog, {
+    manual: true,
+    onSuccess: () => {
+      message.success('编辑日志成功');
+      setWriteVisible(false);
+      run(queryListParams);
+    },
+  });
+
+  const toDetail = async (log_id) => {
+    const data = await approvalLogDetail(log_id);
+    console.log(data);
+    setLogDetail(data);
+    setDetailVisible(true);
+    // runDetail(log_id);
+  };
+  const toEdit = async (log_id) => {
+    const data = await approvalLogDetail(log_id);
+    console.log(data);
+    setLogDetail(data);
+    setWriteVisible(true);
+    // runDetail(log_id);
+  };
+
+  const editLog = (list) => {
+    if (list?.length == 0) return;
+    const params = { id: list[0].log_id, data: list };
+    runEdit(params);
+  };
+
+  // 统一管理 加载状态
+  const allLoading = useMemo(() => {
+    const loadingList = [
+      propsLoading,
+      listLoading,
+      deleteLoading,
+      addLoading,
+      editLoading,
+    ];
+    if (loadingList.find((item) => item)) {
+      return true;
+    }
+    return false;
+  }, [propsLoading, listLoading, deleteLoading, addLoading, editLoading]);
+
+  const deleteRecord = (record) => {
+    confirm({
+      title: '提示',
+      icon: <ExclamationCircleOutlined />,
+      content: '确认删除此条日志?',
+      onOk() {
+        runDelete(record.log_id);
+      },
+    });
+  };
+
+  const columns = [
+    {
+      title: '日志标题',
+      key: 'log_title',
+      width: '20%',
+      render: (record) => {
+        return `${record.author_name}的金科环境项目日志`;
+      },
+    },
+    {
+      title: '提交人',
+      dataIndex: 'author_name',
+      width: '16%',
+    },
+    {
+      title: '提交时间',
+      dataIndex: 'c_time',
+      width: '20%',
+      render: (time) => {
+        if (!time) {
+          return '--';
+        }
+        return dayjs(time).format('YYYY-MM-DD HH:mm');
+      },
+    },
+    {
+      title: '操作',
+      width: '20%',
+      render: (record) => {
+        return (
+          <Space>
+            <a onClick={() => toEdit(record.log_id)}>编辑</a>
+            <a onClick={() => toDetail(record.log_id)}>详情</a>
+            <a onClick={() => deleteRecord(record)}>删除</a>
+          </Space>
+        );
+      },
+    },
+  ];
+
+  // 加载项目列表,如果没有
+  useEffect(() => {
+    if (!projects?.length) {
+      dispatch({ type: 'record/queryProject' });
+    }
+  }, []);
+
+  const handleDataPicked = (_date, dateString) => {
+    let params = '';
+    if (dateString) {
+      params = {
+        s_time: `${dateString} 00:00:00`,
+        e_time: `${dateString} 23:59:59`,
+      };
+    }
+    params.pageSize = 9;
+    params.currentPage = 1;
+    setQueryListParams(params);
+  };
+
+  const handleQuery = () => {
+    run(queryListParams);
+  };
+
+  const handlePageChange = (page, pageSize) => {
+    let params = '';
+    params = queryListParams;
+    params.currentPage = page;
+    params.pageSize = pageSize;
+    run(params);
+    setQueryListParams(params);
+    setPageInfo({ ...pageInfo, current: page, pageSize: pageSize });
+  };
+
+  return (
+    <div>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          marginBottom: '20px',
+        }}
+      >
+        <Space size={24}>
+          <DatePicker onChange={handleDataPicked} />
+          <Button type="primary" onClick={handleQuery}>
+            查询
+          </Button>
+        </Space>
+        <Button
+          type="primary"
+          onClick={() => {
+            setLogDetail([]);
+            setWriteVisible(true);
+          }}
+        >
+          写日志
+        </Button>
+      </div>
+      <Spin spinning={allLoading}>
+        <Table
+          columns={columns}
+          dataSource={data?.list}
+          pagination={{ ...pageInfo, onChange: handlePageChange }}
+        />
+      </Spin>
+      <WriteRecordModal
+        detail={logDetail}
+        visible={writeVisible}
+        user={currentUser}
+        projects={projects}
+        onOk={logDetail ? editLog : runAdd}
+        onCancel={() => setWriteVisible(false)}
+      />
+      <RecordDetailModal
+        visible={detailVisible}
+        onCancel={() => setDetailVisible(false)}
+        detail={logDetail}
+      />
+    </div>
+  );
+};
+
+export default connect(({ user, record, loading }) => ({
+  currentUser: user.currentUser,
+  projects: record.projects,
+}))(DailyRecord);

+ 6 - 0
src/pages/PurchaseList/DailyRecord/index.less

@@ -0,0 +1,6 @@
+.doc {
+  width: 700px;
+  white-space: nowrap; /* 防止文本换行 */
+  overflow: hidden; /* 隐藏溢出的文本 */
+  text-overflow: ellipsis; /* 显示省略号 */
+}

+ 83 - 0
src/pages/PurchaseList/DailyRecord/models/record.js

@@ -0,0 +1,83 @@
+import {
+  queryWorkType,
+  queryWorkHours,
+  queryAuthWorkHours,
+  addWorkHours,
+  addAuthWorkHours,
+  authWorkload,
+  queryProject,
+  deleteWorkHour,
+} from '@/services/workHours';
+import { queryOwnProjects } from '@/services/Daily';
+import { queryDepV2 } from '@/services/approval';
+import { message } from 'antd';
+import dayjs from 'dayjs';
+
+function getDepUserTree(data, map) {
+  data.title = `${data.Name}`;
+  data.key = `${data.ID}`;
+  data.value = `${data.ID}`;
+  map.set(data.ID, data);
+  if (!data.children) data.children = new Array();
+
+  if (data.children) {
+    data.children.forEach((item) => {
+      getDepUserTree(item, map);
+    });
+  }
+  return data;
+}
+
+export default {
+  namespace: 'record',
+  state: {
+    subTypeList: [],
+    projects: [],
+    depUserTree: [],
+  },
+
+  effects: {
+    *querySubType({ payload, callback }, { call, put }) {
+      const { data } = yield call(queryWorkType, payload);
+      callback && callback(data);
+      yield put({
+        type: 'save',
+        payload: { subTypeList: data },
+      });
+    },
+    *queryProject({ payload = {}, callback }, { call, put }) {
+      const res = yield call(queryOwnProjects, payload);
+      if (res) {
+        yield put({
+          type: 'save',
+          payload: {
+            projects: res.data,
+          },
+        });
+        callback && callback();
+      }
+    },
+    *fetchDepV2({ payload, callback }, { call, put }) {
+      const response = yield call(queryDepV2, { pageSize: 999999 });
+      if (response) {
+        const depUserMap = new Map();
+        const depUserTree = response.data.list.map((item) => {
+          return getDepUserTree(item, depUserMap);
+        });
+        yield put({
+          type: 'save',
+          payload: { depUserTree, depUserMap },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 4 - 4
src/pages/PurchaseList/Report/DepCompareModal.js

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import { Modal, Table } from 'antd';
 import { connect } from 'dva';
-import moment from 'moment';
+import dayjs from 'dayjs';
 import { set } from 'lodash';
 
 function DepCompareModal(props) {
@@ -39,7 +39,7 @@ function DepCompareModal(props) {
   const columnsDep = [
     {
       width: 350,
-      render: record =>
+      render: (record) =>
         // record.dep_name || <a onClick={() => onClickUser(record)}>{record.c_name}</a>,
         record.dep_name || record.c_name,
     },
@@ -55,7 +55,7 @@ function DepCompareModal(props) {
     { title: '总工时', dataIndex: 'total_cnt' },
   ];
 
-  const onChangePage = pagination => {
+  const onChangePage = (pagination) => {
     dispatch({
       type: 'report/queryUserReport',
       payload: {
@@ -79,7 +79,7 @@ function DepCompareModal(props) {
     }
   };
 
-  const onClickUser = user => {
+  const onClickUser = (user) => {
     dispatch({
       type: 'report/queryDepUserProject',
       payload: {

+ 1 - 1
src/pages/PurchaseList/Report/Department.js

@@ -14,10 +14,10 @@ import {
 import styles from './report.less';
 import UserRptModal from './UserRptModal';
 import DepCompareModal from './DepCompareModal';
+import dayjs from 'dayjs';
 import { downloadFile, getToken } from '@/utils/utils.js';
 import * as echarts from 'echarts';
 import { CloseOutlined } from '@ant-design/icons';
-import dayjs from 'dayjs';
 
 const { RangePicker } = DatePicker;
 //按天取前月26-当月25

+ 1 - 1
src/pages/PurchaseList/Report/Finance.js

@@ -2,8 +2,8 @@ import React, { useEffect, useState, useRef } from 'react';
 import { connect } from 'dva';
 import { Form, Table, DatePicker, Input, Button, Select } from 'antd';
 import styles from './report.less';
-import { downloadFile, getToken } from '@/utils/utils.js';
 import dayjs from 'dayjs';
+import { downloadFile, getToken } from '@/utils/utils.js';
 
 const { RangePicker } = DatePicker;
 

+ 2 - 2
src/pages/PurchaseList/Report/Finance/Project.js

@@ -12,9 +12,9 @@ import {
 } from 'antd';
 import { ProfileOutlined } from '@ant-design/icons';
 import styles from '../report.less';
+import dayjs from 'dayjs';
 import { downloadFile, getToken } from '@/utils/utils.js';
 import { queryFinanceProjDetail } from '@/services/workHours';
-import dayjs from 'dayjs';
 
 const { RangePicker } = DatePicker;
 
@@ -128,7 +128,7 @@ function FinanceProject(props) {
     return (
       <Form layout="inline" form={form}>
         <Form.Item label="时间" name="time" initialValue={initDate}>
-          <RangePicker placeholder="选择时间" allowClear={false} />
+          {/* <RangePicker placeholder="选择时间" allowClear={false} /> */}
         </Form.Item>
         <Form.Item label="填报人" name="user_id">
           <Select

+ 42 - 20
src/pages/PurchaseList/Report/Project.js

@@ -1,15 +1,24 @@
 import React, { useEffect, useState, useRef } from 'react';
 import { connect } from 'dva';
-import { Form, Table, DatePicker, Input, Button, Select, message, Popover } from 'antd';
+import {
+  Form,
+  Table,
+  DatePicker,
+  Input,
+  Button,
+  Select,
+  message,
+  Popover,
+} from 'antd';
 import report from './models/report';
 import styles from './report.less';
-import moment from 'moment';
+import dayjs from 'dayjs';
 import UserProjectRptModal from './UserProjectRptModal';
 import { downloadFile, getToken } from '@/utils/utils.js';
 
 const { Option } = Select;
 const { RangePicker } = DatePicker;
-const initDate = [moment().startOf('years'), moment()];
+const initDate = [dayjs().startOf('years'), dayjs()];
 
 // var currentYear = new Date().getFullYear();
 // var yearList = [];
@@ -35,17 +44,30 @@ function Resource(props) {
   //   });
   // };
   const getMonthColumns = () => {
-    let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+    let arr = [
+      '一',
+      '二',
+      '三',
+      '四',
+      '五',
+      '六',
+      '七',
+      '八',
+      '九',
+      '十',
+      '十一',
+      '十二',
+    ];
     var time;
     if (filterRef.current.s_time) {
-      time = [moment(filterRef.current.s_time), moment(filterRef.current.e_time)];
+      time = [dayjs(filterRef.current.s_time), dayjs(filterRef.current.e_time)];
     } else {
       time = initDate;
     }
     var date = {};
     let eYear = time[1].year();
     let eMonth = time[1].month();
-    let current = moment(time[0]);
+    let current = dayjs(time[0]);
     let cYear, cMonth;
 
     // do {
@@ -63,16 +85,16 @@ function Resource(props) {
       current.add('month', 1);
     }
     current.subtract('month', 12);
-    let monthColumns = Object.keys(date).map(year => ({
+    let monthColumns = Object.keys(date).map((year) => ({
       title: year + '年',
-      children: date[year].map(item => {
+      children: date[year].map((item) => {
         let key = current.format('YYYY-MM');
         current.add('month', 1);
         return {
           title: `${item}`,
-          render: record => {
+          render: (record) => {
             const { month } = record;
-            return month.find(item => item.st == key)?.pass_audit_cnt || 0;
+            return month.find((item) => item.st == key)?.pass_audit_cnt || 0;
             // return month[key]?.total_audit_cnt || 0;
             // return (
             //   <Popover
@@ -112,16 +134,16 @@ function Resource(props) {
 
   const handleSearch = () => {
     const { time, project_name } = form.getFieldsValue();
-    if (
-      !moment(time[0])
-        .add(12, 'month')
-        .isSameOrAfter(time[1])
-    ) {
+    if (!dayjs(time[0]).add(12, 'month').isSameOrAfter(time[1])) {
       message.error('时间间隔超过12个月,请重新选择。');
       return;
     }
-    filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
-    filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
+    filterRef.current.s_time = time[0]
+      ? dayjs(time[0]).format('YYYY-MM-DD')
+      : null;
+    filterRef.current.e_time = time[1]
+      ? dayjs(time[1]).format('YYYY-MM-DD')
+      : null;
     dispatch({
       type: 'report/queryProjectReport',
       payload: {
@@ -139,7 +161,7 @@ function Resource(props) {
       `/api/v2/workload/rpt/projects/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}&project_name=${
         !project_name ? '' : project_name
       }`,
-      `项目报表${moment().format('YYYYMMDDHHMMSS')}.xlsx`
+      `项目报表${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
     );
   };
   const renderSearch = () => {
@@ -162,7 +184,7 @@ function Resource(props) {
   const columns = [
     {
       title: '项目名称',
-      render: record =>
+      render: (record) =>
         !record.child && record.total_audit_cnt ? (
           <a onClick={() => showUserModal(record)}>{record.name}</a>
         ) : (
@@ -228,7 +250,7 @@ function Resource(props) {
   //     ),
   //   },
   // ];
-  const showUserModal = item => {
+  const showUserModal = (item) => {
     const { s_time, e_time } = filterRef.current;
     setModalFilter({
       s_time: s_time,

+ 1 - 1
src/pages/PurchaseList/Report/ProjectTree.js

@@ -213,7 +213,7 @@ function ProjectTreeModal(props) {
     if (!visible) {
       setList([]);
     }
-  }, [visible, data]);
+  }, [visible]);
 
   return (
     <Modal

+ 33 - 11
src/pages/PurchaseList/Report/UserProjectRptModal.js

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import { Modal, Table, Form, Input, Button } from 'antd';
 import { connect } from 'dva';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 function UserProjectRptModal(props) {
   const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
@@ -19,12 +19,25 @@ function UserProjectRptModal(props) {
   //   };
   // };
   const getMonthColumns = () => {
-    let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
-    var time = [moment(filter.s_time), moment(filter.e_time)];
+    let arr = [
+      '一',
+      '二',
+      '三',
+      '四',
+      '五',
+      '六',
+      '七',
+      '八',
+      '九',
+      '十',
+      '十一',
+      '十二',
+    ];
+    var time = [dayjs(filter.s_time), dayjs(filter.e_time)];
     var date = {};
     let eYear = time[1].year();
     let eMonth = time[1].month();
-    let current = moment(time[0]);
+    let current = dayjs(time[0]);
     let cYear, cMonth;
 
     for (let i = 0; i < 12; i++) {
@@ -35,16 +48,18 @@ function UserProjectRptModal(props) {
       current.add('month', 1);
     }
     current.subtract('month', 12);
-    let monthColumns = Object.keys(date).map(year => ({
+    let monthColumns = Object.keys(date).map((year) => ({
       title: year + '年',
-      children: date[year].map(item => {
+      children: date[year].map((item) => {
         let key = current.format('YYYY-MM');
         current.add('month', 1);
         return {
           title: `${item}`,
-          render: record => {
+          render: (record) => {
             const { month_rpt } = record;
-            return month_rpt.find(item => item.ts == key)?.pass_audit_cnt || 0;
+            return (
+              month_rpt.find((item) => item.ts == key)?.pass_audit_cnt || 0
+            );
           },
         };
       }),
@@ -65,10 +80,11 @@ function UserProjectRptModal(props) {
     {
       title: '总计',
       dataIndex: 'month_rpt',
-      render: arr => (arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : ''),
+      render: (arr) =>
+        arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : '',
     },
   ];
-  const onChangePage = pagination => {
+  const onChangePage = (pagination) => {
     dispatch({
       type: 'report/queryUserProjectReport',
       payload: {
@@ -98,7 +114,13 @@ function UserProjectRptModal(props) {
   }, [filter]);
 
   return (
-    <Modal title="工时" width="80%" visible={visible} onCancel={onCancel} footer={false}>
+    <Modal
+      title="工时"
+      width="80%"
+      visible={visible}
+      onCancel={onCancel}
+      footer={false}
+    >
       <Form layout="inline" style={{ marginBottom: 20 }} form={form}>
         <Form.Item lable="部门" name="dep_name">
           <Input placeholder="请输入部门" />

+ 9 - 3
src/pages/PurchaseList/Report/UserRptModal.js

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import { Modal, Table } from 'antd';
 import { connect } from 'dva';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 function UserRptModal(props) {
   const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
@@ -61,7 +61,7 @@ function UserRptModal(props) {
     //   render: (percent = 0) => (percent * 100).toFixed(2) + '%',
     // },
   ];
-  const onChangePage = pagination => {
+  const onChangePage = (pagination) => {
     dispatch({
       type: 'report/queryUserReport',
       payload: {
@@ -79,7 +79,13 @@ function UserRptModal(props) {
   }, [filter]);
 
   return (
-    <Modal title="工时" width="80%" visible={visible} onCancel={onCancel} footer={false}>
+    <Modal
+      title="工时"
+      width="80%"
+      visible={visible}
+      onCancel={onCancel}
+      footer={false}
+    >
       <Table
         columns={columns}
         loading={loading}

+ 5 - 3
src/pages/PurchaseList/Report/models/report.js

@@ -12,7 +12,9 @@ import {
   queryUserProject,
   queryProjectReportNew,
 } from '@/services/workHours';
+import { queryRole } from '@/services/SysAdmin';
 import { message } from 'antd';
+import dayjs from 'dayjs';
 
 export default {
   namespace: 'report',
@@ -76,7 +78,7 @@ export default {
       //   data.list.forEach(item => {
       //     let month = {};
       //     item.month_rpt.forEach(mItem => {
-      //       let m = moment(mItem.ts).month();
+      //       let m = dayjs(mItem.ts).month();
       //       month[m] = mItem;
       //     });
       //     item.month = month;
@@ -195,7 +197,7 @@ export default {
     //         isParent: true,
     //       };
     //       item.rpt.month_rpt.forEach(mItem => {
-    //         // let m = moment(mItem.ts).month();
+    //         // let m = dayjs(mItem.ts).month();
     //         treeData[p_type_id].month[mItem.ts] = mItem;
     //       });
     //     });
@@ -225,7 +227,7 @@ export default {
       data.forEach((item, index) => {
         let month = {};
         item.month_rpt.forEach((mItem) => {
-          // let m = moment(mItem.ts).month();
+          // let m = dayjs(mItem.ts).month();
           month[mItem.ts] = mItem;
         });
 

+ 2 - 2
src/pages/PurchaseList/WorkingHours/AddModal.js

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import { message, Form, Modal, Select, DatePicker, TreeSelect } from 'antd';
 import { connect } from 'dva';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 const { Option } = Select;
 const { MonthPicker } = DatePicker;
@@ -233,7 +233,7 @@ function AddModal(props) {
   return (
     <Modal
       title="工时"
-      open={visible}
+      visible={visible}
       confirmLoading={loading}
       onCancel={handleCancel}
       maskClosable={false}

+ 17 - 16
src/pages/PurchaseList/WorkingHours/Auth.js

@@ -18,13 +18,12 @@ import AuthWorkList from './AuthWorkList';
 import { connect } from 'dva';
 import styles from './index.less';
 import dayjs from 'dayjs';
-import { useRequest, useModel } from '@umijs/max';
+import { useModel } from '@umijs/max';
 
 function List(props) {
-  const {
-    initialState: { user },
-  } = useModel('@@initialState');
-  const { dispatch, loading, list, project, userList, allType } = props;
+  const { initialState } = useModel('@@initialState');
+  const currentUser = initialState?.user || {};
+  const { dispatch, loading, list, project, allType, user = [] } = props;
   const [visible, setVisible] = useState(false);
   const [filter, setFilter] = useState({});
   const [current, setCurrent] = useState({
@@ -32,6 +31,8 @@ function List(props) {
     list: [],
   });
 
+  console.log('----user----', user);
+
   const rejectRef = useRef({});
 
   const onAgree = (records) => {
@@ -100,8 +101,8 @@ function List(props) {
     if (current.date.format('YYYY-MM') != value.format('YYYY-MM')) {
       const s_date = value.format('YYYY-MM') + '-01';
       const e_date = dayjs(s_date)
-        .add(1, 'month')
-        .add(-1, 'days')
+        .add('month', 1)
+        .add('days', -1)
         .format('YYYY-MM-DD');
       dispatch({
         type: 'workload/queryAuthWorkHours',
@@ -135,15 +136,15 @@ function List(props) {
   };
 
   useEffect(() => {
-    if (!user.ID) return;
+    if (!currentUser.ID) return;
     // 查询分类以及工时
     dispatch({
       type: 'workload/queryWorkType',
       callback: () => {
         const s_date = current.date.format('YYYY-MM') + '-01';
         const e_date = dayjs(s_date)
-          .add(1, 'month')
-          .add(-1, 'days')
+          .add('month', 1)
+          .add('days', -1)
           .format('YYYY-MM-DD');
         dispatch({
           type: 'workload/queryAuthWorkHours',
@@ -180,7 +181,7 @@ function List(props) {
         },
       });
     };
-  }, [user.ID]);
+  }, [currentUser.ID]);
 
   // useEffect(() => {
   //   onChangeDate(current.date);
@@ -192,8 +193,8 @@ function List(props) {
         <Row gutter={8}>
           <Col span={12}>
             <Calendar
-              value={current.date}
-              cellRender={dateCellRender}
+              // value={current.date}
+              // dateCellRender={dateCellRender}
               onChange={onChangeDate}
             />
           </Col>
@@ -204,7 +205,7 @@ function List(props) {
               list={getList()}
               onAgree={onAgree}
               onSearch={setFilter}
-              user={userList}
+              user={user}
               onReject={(records) => {
                 setVisible(true);
                 rejectRef.current = records;
@@ -222,9 +223,9 @@ function List(props) {
   );
 }
 
-export default connect(({ workload, user, loading }) => ({
+export default connect(({ user, workload, loading }) => ({
   list: workload.list,
-  userList: user.list,
+  user: user.list,
   allType: workload.allType,
   project: workload.project,
   loading: loading.models.workload,

+ 8 - 8
src/pages/PurchaseList/WorkingHours/CalendarModal.js

@@ -13,7 +13,7 @@ import {
   Button,
 } from 'antd';
 import styles from './index.less';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 function CalendarModal(props) {
   const {
@@ -107,16 +107,16 @@ function CalendarModal(props) {
 
   useEffect(() => {
     if (data.time.length == 0) return;
-    let month = moment(data.time[0].ts).month();
-    let start = moment().month(month).date(1);
-    let end = moment()
+    let month = dayjs(data.time[0].ts).month();
+    let start = dayjs().month(month).date(1);
+    let end = dayjs()
       .month(month + 1)
       .date(0);
     setValidRange([start, end]);
 
-    let time = moment(data.date).format('YYYY-MM-DD');
+    let time = dayjs(data.date).format('YYYY-MM-DD');
     setCurrent({
-      date: moment(data.date),
+      date: dayjs(data.date),
       list: data.time.filter((item) => item.time == time),
     });
   }, [data.time]);
@@ -125,7 +125,7 @@ function CalendarModal(props) {
     <Modal
       width={1400}
       title={`${data.date}工时`}
-      open={visible}
+      visible={visible}
       confirmLoading={loading}
       onCancel={onCancel}
       maskClosable={false}
@@ -139,7 +139,7 @@ function CalendarModal(props) {
             validRange={validRange}
             value={current.date}
             headerRender={() => {}}
-            cellRender={dateCellRender}
+            dateCellRender={dateCellRender}
             onChange={onChangeDate}
           />
         </Col>

+ 2 - 2
src/pages/PurchaseList/WorkingHours/RejectModal.js

@@ -6,7 +6,7 @@ function RejectModal(props) {
   const [form] = Form.useForm();
   const [type, setType] = useState({});
   const handleOk = () => {
-    form.validateFields().then((values) => {
+    form.validateFields().then(values => {
       onOk(values);
     });
   };
@@ -14,7 +14,7 @@ function RejectModal(props) {
   return (
     <Modal
       title="是否确认拒绝"
-      open={visible}
+      visible={visible}
       confirmLoading={loading}
       onCancel={onCancel}
       maskClosable={false}

+ 16 - 12
src/pages/PurchaseList/WorkingHours/SearchForm.js

@@ -1,6 +1,6 @@
 import React from 'react';
 import { Form, Select, DatePicker, Button } from 'antd';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 const { Option } = Select;
 const { MonthPicker } = DatePicker;
@@ -14,11 +14,11 @@ function SearchForm(props) {
   // const handleSearch = () => {
   //   form.validateFields((error, values) => {
   //     if (error) return;
-  //     var weekOfday = moment(values.time).format('E'); //计算指定日期是这周第几天
-  //     var last_monday = moment(values.time)
+  //     var weekOfday = dayjs(values.time).format('E'); //计算指定日期是这周第几天
+  //     var last_monday = dayjs(values.time)
   //       .subtract(weekOfday - 1, 'days')
   //       .format('YYYY-MM-DD'); //周一日期
-  //     var last_sunday = moment(values.time)
+  //     var last_sunday = dayjs(values.time)
   //       .add(7 - weekOfday, 'days')
   //       .format('YYYY-MM-DD'); //周日日期
 
@@ -31,12 +31,10 @@ function SearchForm(props) {
   //   });
   // };
 
-  const handleChange = data => {
-    var month = moment(data).month(); //计算指定日期是这周第几天
-    var start = moment(data)
-      .date(1)
-      .format('YYYY-MM-DD'); //本月第一天
-    var end = moment(data)
+  const handleChange = (data) => {
+    var month = dayjs(data).month(); //计算指定日期是这周第几天
+    var start = dayjs(data).date(1).format('YYYY-MM-DD'); //本月第一天
+    var end = dayjs(data)
       .month(month + 1)
       .date(0)
       .format('YYYY-MM-DD'); //本月最后一天
@@ -80,8 +78,14 @@ function SearchForm(props) {
       )} */}
 
       <Form.Item label="时间">
-        {form.getFieldDecorator('time', {})(
-          <MonthPicker onChange={data => handleChange(data)} placeholder="选择月份" />
+        {form.getFieldDecorator(
+          'time',
+          {},
+        )(
+          <MonthPicker
+            onChange={(data) => handleChange(data)}
+            placeholder="选择月份"
+          />,
         )}
       </Form.Item>
       {/* <Form.Item>

+ 43 - 27
src/pages/PurchaseList/WorkingHours/index.js

@@ -13,7 +13,8 @@ import AddModal from './AddModal';
 import WorkList from './WorkList';
 import { connect } from 'dva';
 import dayjs from 'dayjs';
-import { useRequest, useModel } from '@umijs/max';
+import { queryWorkHours } from '@/services/workHours';
+import { useModel } from '@umijs/max';
 
 function List(props) {
   const {
@@ -27,9 +28,8 @@ function List(props) {
     depUserTree,
     depUserMap,
   } = props;
-  const {
-    initialState: { user },
-  } = useModel('@@initialState');
+  const { initialState } = useModel('@@initialState');
+  const currentUser = initialState?.user || {};
   const [visible, setVisible] = useState(false);
   const [current, setCurrent] = useState({
     date: dayjs(),
@@ -77,16 +77,28 @@ function List(props) {
   };
 
   const MultiAuth = () => {
+    const curMonth = dayjs(current.date).format('MM');
+    const upMonth = dayjs(curMonth).add('month', -1).format('MM');
     Modal.confirm({
       title: '提示',
-      content: '是否上报全部审批?',
+      content: `是否上报${upMonth}月26至${curMonth}月25全部工时?`,
       okText: '确认',
       cancelText: '取消',
-      onOk() {
+      onOk: async () => {
         let params = [];
-        debugger;
-        for (let i = 0; i < dataList.length; i++) {
-          const element = dataList[i];
+        const e_time = current.date.format('YYYY-MM') + '-25 23:59:59';
+        const s_time =
+          dayjs(e_time).add('month', -1).format('YYYY-MM') + '-26 00:00:00';
+        const queryData = {
+          s_time,
+          e_time,
+          pageSize: 9999,
+          user_id: currentUser.ID,
+        };
+        const res = await queryWorkHours(queryData);
+        console.log(res.data.list);
+        for (let i = 0; i < res.data.list.length; i++) {
+          const element = res.data.list[i];
           if (element.audit_state != 0) continue;
           var arr = params.find((arr) => arr.type_id == element.type_id);
           if (arr) {
@@ -243,9 +255,8 @@ function List(props) {
     });
   };
 
-  const dateCellRender = (value, info) => {
+  const dateCellRender = (value) => {
     let current = value.format('YYYY-MM-DD');
-    // console.log(value);
     let list = dataList.filter(
       (item) => item.time == current && item.status == 0,
     );
@@ -259,7 +270,7 @@ function List(props) {
         waitTotal += item.workload;
       }
     });
-    if (list.length == 0) return info.origin;
+    if (list.length == 0) return;
     let content = (
       <div>
         {list.map((item) => (
@@ -280,19 +291,23 @@ function List(props) {
   };
 
   const onChangeDate = (value) => {
-    console.log(value);
     let time = value.format('YYYY-MM-DD');
     if (current.date.format('YYYY-MM') != value.format('YYYY-MM')) {
       const s_date = value.format('YYYY-MM') + '-01';
       const e_date = dayjs(s_date)
-        .add(1, 'month')
-        .add(-1, 'days')
+        .add('month', 1)
+        .add('days', -1)
         .format('YYYY-MM-DD');
+      // const e_date = value.format('YYYY-MM') + '-25';
+      // const s_date =
+      //   dayjs(e_date)
+      //     .add('month', -1)
+      //     .format('YYYY-MM') + '-26';
       dispatch({
         type: 'workload/queryWorkHours',
         payload: {
           pageSize: 9999,
-          user_id: user.ID,
+          user_id: currentUser.ID,
           s_time: s_date + ' 00:00:00',
           e_time: e_date + ' 23:59:59',
         },
@@ -324,21 +339,26 @@ function List(props) {
   };
 
   useEffect(() => {
-    if (!user.ID) return;
+    if (!currentUser.ID) return;
     // 查询分类以及工时
     dispatch({
       type: 'workload/queryWorkType',
       callback: () => {
         const s_date = current.date.format('YYYY-MM') + '-01';
         const e_date = dayjs(s_date)
-          .add(1, 'month')
-          .add(-1, 'days')
+          .add('month', 1)
+          .add('days', -1)
           .format('YYYY-MM-DD');
+        // const e_date = current.date.format('YYYY-MM') + '-25';
+        // const s_date =
+        //   dayjs(e_date)
+        //     .add('month', -1)
+        //     .format('YYYY-MM') + '-26';
         dispatch({
           type: 'workload/queryWorkHours',
           payload: {
             pageSize: 9999,
-            user_id: user.ID,
+            user_id: currentUser.ID,
             s_time: s_date + ' 00:00:00',
             e_time: e_date + ' 23:59:59',
           },
@@ -366,17 +386,14 @@ function List(props) {
         },
       });
     };
-  }, [user.ID]);
-  console.log(current.date);
+  }, [currentUser.ID]);
 
-  // useEffect(() => {
-  //   onChangeDate(current.date);
-  // }, [dataList]);
   useEffect(() => {
     dispatch({
       type: 'workload/fetchDepV2',
     });
   }, []);
+
   return (
     <div>
       <Spin spinning={loading}>
@@ -439,11 +456,10 @@ function List(props) {
   );
 }
 
-export default connect(({ workload, user, loading }) => ({
+export default connect(({ workload, loading }) => ({
   dataList: workload.dataList,
   typeList: workload.typeList,
   allType: workload.allType,
-  // currentUser: user.currentUser,
   loading: loading.models.workload,
   depUserTree: workload.depUserTree,
   depUserMap: workload.depUserMap,

+ 7 - 6
src/pages/PurchaseList/WorkingHours/models/workingHours.js

@@ -7,10 +7,11 @@ import {
   authWorkload,
   queryProject,
   deleteWorkHour,
-} from '@/services/workHours.js';
+} from '@/services/workHours';
+import { queryRole } from '@/services/SysAdmin';
 import { queryDepV2 } from '@/services/approval';
 import { message } from 'antd';
-import moment from 'moment';
+import dayjs from 'dayjs';
 
 function getDepUserTree(data, map) {
   data.title = `${data.Name}`;
@@ -83,12 +84,12 @@ export default {
       // let tempList = {};
       // // 根据userid与date将所有数据分类
       data.list.forEach((item) => {
-        // let date = moment(item.ts).format('YYYY-MM');
+        // let date = dayjs(item.ts).format('YYYY-MM');
         // let userId = item.User.ID;
         // if (!tempList[userId]) tempList[userId] = {};
         // if (!tempList[userId][date]) tempList[userId][date] = [];
         // tempList[userId][date].push(item);
-        item.time = moment(item.ts).format('YYYY-MM-DD');
+        item.time = dayjs(item.ts).format('YYYY-MM-DD');
       });
       // let workList = [];
       // // 重组数据结构
@@ -125,10 +126,10 @@ export default {
       let { data } = yield call(queryWorkHours, newFilter);
       // let tempList = {};
       data.list.forEach((item) => {
-        // let date = moment(item.ts).format('YYYY-MM');
+        // let date = dayjs(item.ts).format('YYYY-MM');
         // if (!tempList[date]) tempList[date] = [];
         // tempList[date].push(item);
-        item.time = moment(item.ts).format('YYYY-MM-DD');
+        item.time = dayjs(item.ts).format('YYYY-MM-DD');
       });
       // let workList = [];
       // Object.keys(tempList).forEach(date => {

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

@@ -0,0 +1,101 @@
+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(26) // 26号的9点为止
+      .hour(9)
+      .minute(0)
+      .second(0)
+      .millisecond(0);
+
+    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;

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

@@ -0,0 +1,148 @@
+import React, { useMemo, useState } from 'react';
+import { Button, Modal, Table, Tooltip } from 'antd';
+import * as XLSX from 'xlsx';
+
+const ReportTable = ({ data, month }) => {
+  const [visible, setVisible] = useState(false);
+  const [item, setItem] = useState({});
+  // 将 data 对象解析为表格的 dataSource
+  const dataSource = useMemo(() => {
+    if (!data) return [];
+    // return data
+    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: item => <a onClick={() => showModal(item)}>日志提交时间</a>,
+    },
+  ];
+
+  const showModal = item => {
+    setItem(item);
+    setVisible(true);
+  };
+
+  return (
+    <>
+      <Table
+        dataSource={dataSource}
+        footer={() => (
+          <Button onClick={exportToExcel} type="primary">
+            导出报表
+          </Button>
+        )}
+        columns={columns}
+      />
+      <Modal
+        title="日志提交记录"
+        visible={visible}
+        onCancel={() => setVisible(false)}
+        footer={null}
+      >
+        {item.dates?.map((item, index) => (
+          <div style={{ margin: '5px 0' }} key={index}>
+            {item.format('YYYY-MM-DD HH:mm:ss')}
+          </div>
+        ))}
+      </Modal>
+    </>
+  );
+};
+
+export default ReportTable;

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

@@ -0,0 +1,170 @@
+const path = require('path');
+const moment = require('moment');
+import {
+  getHoliday,
+  getUserTakingLeave,
+  getAllReport,
+  getHiredDate,
+  getResignationDate,
+  getUserTakingDetail,
+} 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 (item.creator_name != '张西明') 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);
+  });
+  // debugger
+  // 遍历工号,检查每个工号对应的提交时间
+  for (const employeeId in employeeSubmissions) {
+    const submissions = employeeSubmissions[employeeId].dates;
+    console.log(`请求${employeeSubmissions[employeeId].name}的请假详情`);
+    onProcess?.(`请求${employeeSubmissions[employeeId].name}的请假详情`);
+    // 查询请假情况
+    const takingLeave = await getUserTakingDetail(startDate, endDate, employeeId);
+
+    // 根据日志提交时间、节假日、请假情况,获取未提交以及漏交记录
+    const { lateSubmissions, unsubmittedReports, takingLeaveReports } = await analyzeDates(
+      submissions,
+      startDate,
+      endDate,
+      holiday,
+      takingLeave
+    );
+    employeeSubmissions[employeeId].lateSubmissions = lateSubmissions;
+    employeeSubmissions[employeeId].unsubmittedReports = unsubmittedReports;
+    employeeSubmissions[employeeId].takingLeaveReports = takingLeaveReports;
+  }
+
+  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();
+
+  // 日志查询至26号9点,实际截至时间为25号24点
+  // 所以结束时间为endDate - 1天
+  let end = moment(endDate).subtract(1, 'days')
+
+  while (currentDay.isSameOrBefore(end, 'day')) {
+    let index = -1;
+    let dayKey = currentDay.format('YYYY-MM-DD');
+    if (holiday[dayKey] || currentDay.day() === 0 || currentDay.day() === 6) {
+      // 节假日与周末不做处理
+    } else {
+      // 一日可能有多条记录,通过遍历找到今日最后一次提交记录
+      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) {
+        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);
+  if (userIds.length == 0) return;
+  // 根据未提交人的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,
+  });
+  debugger
+  // 判断员工入职时间和离职时间
+  await filterByHireDate(employeeData, onProcess);
+  await filterByResignationDate(employeeData, onProcess);
+
+  return employeeData;
+}

+ 50 - 0
src/services/Daily.js

@@ -0,0 +1,50 @@
+import request from '@/utils/request';
+import { stringify } from 'qs';
+
+export async function getList(params) {
+  return request(`/daily/list/${params.ProjectId}?${stringify(params)}`);
+}
+export async function getDaily(params) {
+  return request(`/daily/list/${params.ProjectId}?${stringify(params)}`);
+}
+
+export async function addDaily(params) {
+  return request(`/daily/item`, { method: 'POST', body: { ...params } });
+}
+
+export async function updateDaily(params = {}) {
+  return request(`/daily/item`, {
+    method: 'PUT',
+    body: {
+      ...params,
+    },
+  });
+}
+
+export async function removeDaily(params) {
+  return request(`/daily/item/${params.Id}`, { method: 'DELETE' });
+}
+
+export async function getDetail(params) {
+  return request(`/daily/item/${params.detailId}`);
+}
+
+export async function getDailyDetail(params) {
+  return request(`/daily/item/${params.detailId}`);
+}
+
+export async function getDailyFiles(params) {
+  return request(`/daily/item/${params.detailId}/file`);
+}
+
+export async function removeDailyFile(params) {
+  return request(`/daily/file/${params.Id}`, { method: 'DELETE' });
+}
+
+export async function downloadSource(params) {
+  return request(`/excel/project-daily/${params.detailId}?${stringify(params)}`);
+}
+
+export async function queryOwnProjects() {
+  return request(`/api/v2/approval/list/simple`);
+}

+ 310 - 0
src/services/ReportDaily.js

@@ -0,0 +1,310 @@
+import axios from 'axios';
+const moment = require('moment');
+
+let api = axios.create({
+  baseURL: 'http://47.96.12.136:8123/api',
+  // baseURL: 'http://192.168.20.45:8123/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
+ * @param {string} userid
+ * @returns
+ */
+export async function getUserTakingDetail(startDate, endDate, userid) {
+  if (!access_token) {
+    access_token = await getToken();
+  }
+
+  // 钉钉的接口 URL
+  const url = '/getleavestatus?access_token=' + access_token;
+  const size = 20;
+
+  const requestData = {
+    start_time: startDate.valueOf(),
+    offset: 0,
+    size,
+    end_time: endDate.valueOf(),
+    userid_list: userid,
+  };
+
+  // 设置请求头
+  const headers = {
+    'Content-Type': 'application/json',
+    Authorization: `Bearer ${access_token}`,
+  };
+  function getDaysInRange(startDate, endDate) {
+    const start = new Date(startDate);
+    const end = new Date(endDate);
+    const days = {};
+
+    // 循环遍历每一天,包括开始日期和结束日期
+    for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {
+      const formattedDate = moment(date).format('YYYY-MM-DD');
+      days[formattedDate] = 1;
+    }
+
+    return days;
+  }
+  const getDataByPage = async offset => {
+    let catchKey = `getleavestatus|${userid}|${offset}|${requestData.start_time}`;
+    let res = {};
+    let catchData = localStorage[catchKey];
+    if (catchData) {
+      res = JSON.parse(catchData);
+    } else {
+      // 发送 POST 请求
+      let response = await api.post(url, { ...requestData, offset }, { headers });
+      res = response.data.result;
+      res.leave_status = res.leave_status.map(item => ({
+        userid: item.userid,
+        start_time: item.start_time,
+        end_time: item.end_time,
+      }));
+
+      localStorage[catchKey] = JSON.stringify(res);
+    }
+
+    return res;
+  };
+  let res = null,
+    leave_status = {},
+    count = 1;
+  do {
+    res = await getDataByPage(res ? (count - 1) * size : 0);
+    res.leave_status.forEach(item => {
+      
+      let days = getDaysInRange(item.start_time, item.end_time);
+      Object.assign(leave_status, days);
+    });
+    count++;
+  } while (res.has_more);
+
+  return leave_status;
+}
+
+/**
+ * 根据时间段查询日志提交情况
+ * @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 = {
+      "2023-07-07": 1
+    };
+  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文件。");
+//   });
+// });

+ 7 - 2
src/services/approval.js

@@ -111,10 +111,10 @@ export async function startQuality(data) {
 //移除成员
 export async function deleteMember(data) {
   return request(
-    `/api/v2/project_code/user/${data.project_code_id}/${data.user_id}`,
+    `/api/v2/project_code/user/${data.project_code_id}/${data.user_id}?flow_id=${data.flow_id}`,
     {
       method: 'DELETE',
-    },
+    }
   );
 }
 
@@ -138,3 +138,8 @@ export async function modifyManager(params) {
     data: params,
   });
 }
+
+//历史状态接口
+export async function queryStatusHistory({ id }) {
+  return request(`/api/v2/approval/status/history?id=${id}`);
+}

+ 69 - 0
src/services/record.js

@@ -0,0 +1,69 @@
+import request from '@/utils/request';
+import { stringify } from 'qs';
+// 新增日志
+export async function approvalAddLog(params) {
+  return request(`/api/v2/approval/log`, {
+    method: 'POST',
+    body: params,
+  });
+}
+
+// 删除日志
+export async function approvalDeleteLog(logID) {
+  return request(`/api/v2/approval/log/${logID}`, {
+    method: 'DELETE',
+  });
+}
+
+// 编辑日志
+export async function approvalEditLog(params) {
+  return request(`/api/v2/approval/log/${params.id}`, {
+    method: 'PUT',
+    body: params.data,
+  });
+}
+// 请求个人日志列表
+export async function approvalLogOwnList(params) {
+  const res = await request(`/api/v2/approval/log-own/list?${stringify(params)}`);
+  return res?.data || { list: [], pagination: {} };
+}
+
+/**
+ * 查询日志详情
+ * @param {string} log_id
+ * @returns Promise<logDetail[]>
+ */
+export async function approvalLogDetail(logID) {
+  const res = await request(`/api/v2/approval/log/${logID}`);
+  return res?.data || [];
+}
+
+//项目日志列表
+export async function approvalLogProjectsList(params) {
+  const res = await request(`/api/v2/approval/log-projects/${params.id}?${stringify(params)}`);
+  return res?.data;
+}
+
+//统计分析接口
+export async function workloadDepProjectChart() {
+  const res = await request(`/api/v2/workload/dep/project/chart`);
+  // if (res?.data) {
+  //   const data = res?.data;
+  //   data.pieData = data?.status_chart?.map(item => {
+  //     return { value: item.num, name: item.status };
+  //   });
+  //   return data;
+  // }
+  return res?.data;
+}
+//统计分析接口 项目分类统计
+export async function workloadDepProjectTypeChart(params) {
+  const res = await request(`/api/v2/workload/dep/project-type/chart?${stringify(params)}`);
+  // if (res?.data) {
+  //   const newData = res.data;
+  //   newData.xData = res.data.map(item => item.create_time);
+  //   newData.dataList = { name: '项目分类统计', data: res.data.map(item => item.num) };
+  //   return newData;
+  // }
+  return res?.data;
+}

+ 8 - 1
yarn.lock

@@ -6731,6 +6731,13 @@ locate-path@^6.0.0:
   dependencies:
     p-locate "^5.0.0"
 
+lodash-decorators@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/lodash-decorators/-/lodash-decorators-6.0.1.tgz#f5347811ee7792eba4719042354541578142273d"
+  integrity sha512-1M0YC8G3nFTkejZEk2ehyvryEdcqj6xATH+ybI8j53cLs/bKRsavaE//y7nz/A0vxEFhxYqev7vdWfsuTJ1AtQ==
+  dependencies:
+    tslib "^1.9.2"
+
 lodash-es@^4.17.15:
   version "4.17.21"
   resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
@@ -10095,7 +10102,7 @@ tslib@2.3.0:
   resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
   integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
 
-tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.2:
   version "1.14.1"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==