ソースを参照

合并人日项目到数字化

Renxy 2 年 前
コミット
221cfc1b7c
40 ファイル変更5748 行追加533 行削除
  1. 97 2
      .umirc.ts
  2. 1 0
      package.json
  3. 4 3
      src/app.tsx
  4. 91 77
      src/global.less
  5. 299 0
      src/pages/PurchaseList/Approval/ApprovalModal.js
  6. 365 0
      src/pages/PurchaseList/Approval/Auth.js
  7. 84 0
      src/pages/PurchaseList/Approval/AuthModal.js
  8. 85 0
      src/pages/PurchaseList/Approval/BudgetModal.js
  9. 133 0
      src/pages/PurchaseList/Approval/DetailModal.js
  10. 16 0
      src/pages/PurchaseList/Approval/DetailModal.less
  11. 72 0
      src/pages/PurchaseList/Approval/ExecutionModal.js
  12. 614 0
      src/pages/PurchaseList/Approval/List.js
  13. 10 0
      src/pages/PurchaseList/Approval/List.less
  14. 152 0
      src/pages/PurchaseList/Approval/MemberModal.js
  15. 63 0
      src/pages/PurchaseList/Approval/ModifyManagerModal.js
  16. 75 0
      src/pages/PurchaseList/Approval/QualityOperateModal.js
  17. 283 0
      src/pages/PurchaseList/Approval/models/approval.js
  18. 497 0
      src/pages/PurchaseList/Approval/provinces.js
  19. 0 153
      src/pages/PurchaseList/Index.js
  20. 304 0
      src/pages/PurchaseList/Report/Demo.js
  21. 175 0
      src/pages/PurchaseList/Report/DepCompareModal.js
  22. 363 0
      src/pages/PurchaseList/Report/Department.js
  23. 159 0
      src/pages/PurchaseList/Report/Finance.js
  24. 257 0
      src/pages/PurchaseList/Report/Finance/Project.js
  25. 112 0
      src/pages/PurchaseList/Report/Finance/Resources.js
  26. 47 0
      src/pages/PurchaseList/Report/Finance/models/finance.js
  27. 280 0
      src/pages/PurchaseList/Report/Project.js
  28. 240 0
      src/pages/PurchaseList/Report/ProjectTree.js
  29. 139 0
      src/pages/PurchaseList/Report/Resource.js
  30. 126 0
      src/pages/PurchaseList/Report/UserProjectRptModal.js
  31. 97 0
      src/pages/PurchaseList/Report/UserRptModal.js
  32. 326 0
      src/pages/PurchaseList/Report/models/report.js
  33. 13 0
      src/pages/PurchaseList/Report/report.less
  34. 0 61
      src/pages/PurchaseList/RightContent.js
  35. 17 15
      src/pages/PurchaseList/WorkingHours/Auth.js
  36. 0 1
      src/pages/PurchaseList/WorkingHours/models/workingHours.js
  37. 0 112
      src/pages/PurchaseList/WorkloadIndex.js
  38. 129 106
      src/services/approval.js
  39. 3 3
      src/services/workHours.js
  40. 20 0
      yarn.lock

+ 97 - 2
.umirc.ts

@@ -184,10 +184,105 @@ export default defineConfig({
     },
     {
       name: '人日立项',
-      path: '/work-hours',
-      component: './PurchaseList/WorkingHours/index',
+      path: '/workload',
       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',
+          routes: [
+            {
+              name: '上报工时',
+              path: 'work-hours',
+              component: './PurchaseList/WorkingHours/index',
+            },
+            {
+              name: '审批工时',
+              path: 'work-hours-auth',
+              component: './PurchaseList/WorkingHours/Auth',
+            },
+          ],
+        },
+        {
+          name: '项目立项',
+          path: '/workload/approval',
+          icon: 'https://gt-digitization.oss-cn-hangzhou.aliyuncs.com/doc/department/2023-04/manufacturerIcon.png',
+          routes: [
+            {
+              name: '项目列表',
+              path: 'list',
+              component: './PurchaseList/Approval/List',
+            },
+            {
+              name: '审核列表',
+              path: 'auth',
+              component: './PurchaseList/Approval/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/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',
+            },
+          ],
+        },
+      ],
     },
+
+    // {checkReport(3) && (
+    //   // <Menu.Item key="/home/report/finance">
+    //   //   <Link to="/home/report/finance">财务报表</Link>
+    //   // </Menu.Item>
+    //   <SubMenu key="/home/report/finance" title="财务报表">
+    //     <Menu.Item key="/home/report/finance">
+    //       <Link to="/home/report/finance">资源总表</Link>
+    //     </Menu.Item>
+    //     <Menu.Item key="/home/report/finance/resources">
+    //       <Link to="/home/report/finance/resources">
+    //         资源中心人日使用汇总表
+    //       </Link>
+    //     </Menu.Item>
+    //     <Menu.Item key="/home/report/finance/project">
+    //       <Link to="/home/report/finance/project">
+    //         执行项目人日汇总表
+    //       </Link>
+    //     </Menu.Item>
+    //   </SubMenu>
+    // )}
   ],
   npmClient: 'yarn',
 });

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "@umijs/max": "^4.0.64",
     "antd": "^5.0.0",
     "dayjs": "^1.11.7",
+    "echarts": "^5.4.2",
     "qs": "^6.11.1",
     "react-file-viewer": "^1.2.1",
     "react-zmage": "^0.8.5-beta.36",

+ 4 - 3
src/app.tsx

@@ -34,7 +34,7 @@ export async function getInitialState(): Promise<any> {
 
 export const layout: RunTimeLayoutConfig = (initialState) => {
   return {
-    navTheme: 'light',
+    // navTheme: 'light',
     layout: 'mix',
     title: '',
     token: {
@@ -55,8 +55,9 @@ export const layout: RunTimeLayoutConfig = (initialState) => {
         colorBgMenuItemSelected: 'rgba(230,243,254,1)',
       },
     },
-    // fixedHeader: true,
-    // fixSiderbar: true,
+    splitMenus: true,
+    fixedHeader: true,
+    fixSiderbar: true,
     rightRender: () => <UserDropdown />,
     logo: logo,
   };

+ 91 - 77
src/global.less

@@ -56,7 +56,7 @@ ol {
 // Remove link styles
 a {
   text-decoration: none;
-  color: inherit;
+  // color: inherit;
 }
 
 // Remove table border
@@ -109,80 +109,94 @@ input[type='reset'] {
   margin-inline: 0;
   height: 79px;
 }
-.ant-pro-layout .ant-layout-header.ant-pro-layout-header {
-  background-color: transparent;
-  height: 79px;
-}
-.ant-layout .ant-layout-header {
-  height: 79px !important;
-  line-height: 79px !important;
-}
-.ant-pro-global-header-logo img {
-  height: 40px;
-}
-.ant-pro-global-header-logo-mix {
-  margin-left: 62px;
-}
-
-.ant-layout-sider {
-  height: calc(100% - 79px) !important;
-  inset-block-start: 79px !important;
-  background-size: 132% 100% !important;
-  background-image: url(@/assets/SideMenu/background.png) !important;
-}
-
-.ant-menu-title-content {
-  font-size: 18px;
-  color: #e8f7fc;
-}
-
-.ant-pro-base-menu-icon {
-  height: 20px;
-  width: 20px;
-}
-.ant-pro-base-menu-item-icon {
-  margin-right: 10px;
-}
-
-.ant-menu-item,
-.ant-menu-submenu {
-  margin-top: 15px !important;
-  margin-bottom: 15px !important;
-}
-
-.ant-menu-item,
-.ant-menu-submenu-title {
-  border-bottom: 1px solid !important;
-  border-image: linear-gradient(#3e6ab4, #5bbdfa) 2 2 2 2 !important;
-}
-
-.ant-menu-sub {
-  margin-left: 32px !important;
-  .ant-menu-item {
-    margin-top: 10px !important;
-    margin-bottom: 10px !important;
-  }
-  .ant-menu-title-content {
-    font-size: 16px;
-    color: #e8f7fc;
-  }
-}
-
-.ant-menu-submenu-arrow {
-  color: #e8f7fc !important;
-}
-
-.ant-menu-item-selected,
-.ant-pro-base-menu-collapsed
-  .ant-menu-submenu-selected
-  > .ant-menu-submenu-title,
-.ant-menu-item:active {
-  background-image: url(@/assets/SideMenu/selectedBackground.png);
-  background-color: transparent !important;
-}
-
-.ant-menu-submenu-popup {
-  .ant-menu {
-    background-color: #1b366f !important;
-  }
+// .ant-pro-layout .ant-layout-header.ant-pro-layout-header {
+//   background-color: transparent;
+//   height: 79px;
+// }
+// .ant-layout .ant-layout-header {
+//   height: 79px !important;
+//   line-height: 79px !important;
+// }
+// .ant-pro-global-header-logo img {
+//   height: 40px;
+// }
+// .ant-pro-global-header-logo-mix {
+//   margin-left: 62px;
+// }
+
+// .ant-layout-sider {
+//   height: calc(100% - 79px) !important;
+//   inset-block-start: 79px !important;
+//   background-size: 132% 100% !important;
+//   background-image: url(@/assets/SideMenu/background.png) !important;
+// }
+
+// .ant-menu-title-content {
+//   font-size: 18px;
+//   color: #e8f7fc;
+// }
+
+// .ant-pro-base-menu-icon {
+//   height: 20px;
+//   width: 20px;
+// }
+// .ant-pro-base-menu-item-icon {
+//   margin-right: 10px;
+// }
+
+// .ant-menu-item,
+// .ant-menu-submenu {
+//   margin-top: 15px !important;
+//   margin-bottom: 15px !important;
+// }
+
+// .ant-menu-item,
+// .ant-menu-submenu-title {
+//   border-bottom: 1px solid !important;
+//   border-image: linear-gradient(#3e6ab4, #5bbdfa) 2 2 2 2 !important;
+// }
+
+// .ant-menu-sub {
+//   margin-left: 32px !important;
+//   .ant-menu-item {
+//     margin-top: 10px !important;
+//     margin-bottom: 10px !important;
+//   }
+  // .ant-menu-title-content {
+  //   font-size: 16px;
+  //   color: #e8f7fc;
+  // }
+// }
+
+// .ant-menu-submenu-arrow {
+//   color: #e8f7fc !important;
+// }
+
+// .ant-menu-item-selected,
+// .ant-pro-base-menu-collapsed
+//   .ant-menu-submenu-selected
+//   > .ant-menu-submenu-title,
+// .ant-menu-item:active {
+//   background-image: url(@/assets/SideMenu/selectedBackground.png);
+//   background-color: transparent !important;
+// }
+
+// .ant-menu-submenu-popup {
+//   .ant-menu {
+//     background-color: #1b366f !important;
+//   }
+// }
+::-webkit-scrollbar {
+  width: 12px;
+  height: 14px;
+  border-radius: 10px;
+  background: rgba(40, 102, 178, 0.2);
+}
+
+/*滚动条滑块*/
+::-webkit-scrollbar-thumb {
+  height: 14px;
+  border-radius: 10px;
+  background-color: rgba(40, 102, 178, 1);
+  // background: linear-gradient(270deg, #99e7ff 4%, #60a8ff 100%);
 }

+ 299 - 0
src/pages/PurchaseList/Approval/ApprovalModal.js

@@ -0,0 +1,299 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
+import moment from 'moment';
+import provinces from './provinces';
+import { queryApproval } from '@/services/approval';
+const { Option } = Select;
+const { TreeNode } = TreeSelect;
+// 新建
+function AddModal(props) {
+  const {
+    total,
+    dispatch,
+    visible,
+    onClose,
+    onOk,
+    data,
+    currentUser,
+    depUserTree,
+    flowList = [],
+    industryList = [],
+    typeList = [],
+    disabled,
+    loading,
+  } = props;
+  const [form] = Form.useForm();
+  const [codes, setCodes] = useState({
+    type: '',
+    industry: '',
+    location: '',
+    name: '',
+    version: '',
+  });
+  const [type, setType] = useState({});
+
+  const handleOk = () => {
+    form.validateFields().then((fieldsValue) => {
+      let values = { ...fieldsValue, id: data.id };
+      values.project_name = fieldsValue.project_name;
+      values.flow_id = Number(fieldsValue.flow_id);
+      values.type_id = Number(fieldsValue.type_id);
+      // 获得flow下第一个node的id
+      values.node_id = flowList.find(
+        (item) => item.id == values.flow_id,
+      ).Nodes[0].id;
+      //项目分类为不为研发时
+      if (fieldsValue.type_id != 7) {
+        values.industry_id = Number(fieldsValue.industry_id);
+        let [location, location_code] = fieldsValue.location.split('##');
+        values.location = location;
+        values.location_code = location_code;
+        values.project_full_code = `${codes.type}${codes.industry}${codes.location}${codes.name}${codes.version}`;
+      }
+      if (fieldsValue.author) {
+        values.author = Number(fieldsValue.author.split('||')[0]);
+        values.author_dep_id = Number(fieldsValue.author.split('||')[1]);
+      } else {
+        values.author = null;
+      }
+      onOk(values);
+    });
+  };
+
+  const renderTreeNodes = (data) => {
+    return data.map((item) => {
+      let title = item.name;
+      let code = item.code || '';
+      if (code.length == 4) {
+        code = code.substr(1);
+      }
+      if (code) {
+        title += `(${code})`;
+      }
+      let key = `${item.name}##${code}`;
+      return (
+        <TreeNode
+          title={title}
+          key={key}
+          value={key}
+          dataRef={item}
+          selectable={Boolean(code)}
+        >
+          {item.children && renderTreeNodes(item.children || [])}
+        </TreeNode>
+      );
+    });
+  };
+  const changeType = (id) => {
+    const item = typeList.find((item) => item.id == id);
+    setCodes({
+      ...codes,
+      type: item.code,
+    });
+    setType(item);
+    form.setFieldsValue({ flow_id: id == 7 ? '4' : '1' });
+  };
+  const changeIndustry = (id) => {
+    const item = industryList.find((item) => item.id == id);
+    setCodes({
+      ...codes,
+      industry: item.code,
+    });
+  };
+  const changeVersion = (index) => {
+    setCodes({
+      ...codes,
+      version: index,
+    });
+  };
+  const changeLocation = (value) => {
+    const [location, code] = value.split('##');
+    setCodes({
+      ...codes,
+      location: code,
+    });
+  };
+  const onBlurName = (e) => {
+    let value = e.target.value.toUpperCase();
+    while (value.length < 3) {
+      value = value + 'V';
+    }
+    form.setFieldsValue({
+      name: value,
+    });
+    setCodes({
+      ...codes,
+      name: value,
+    });
+  };
+
+  const renderDetail = () => {
+    return (
+      <>
+        <Form.Item
+          label="行业名称"
+          name="industry_id"
+          initialValue={String(data.industry_id || '')}
+          rules={[{ required: true, message: '请选择行业名称' }]}
+        >
+          <Select style={{ width: '100%' }} onChange={changeIndustry}>
+            {industryList.map((item) => (
+              <Option key={item.id}>
+                {item.name}({item.code})
+              </Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item
+          label="项目地区"
+          name="location"
+          initialValue={data.location}
+          rules={[{ required: true, message: '请选择项目地区' }]}
+        >
+          <TreeSelect
+            dropdownStyle={{ maxHeight: 300, overflow: 'auto' }}
+            onChange={changeLocation}
+          >
+            {renderTreeNodes(provinces)}
+          </TreeSelect>
+        </Form.Item>
+        <Form.Item
+          label="项目简称"
+          name="name"
+          initialValue={data.name}
+          rules={[
+            { required: true, message: '请输入项目简称' },
+            {
+              validator: (rule, value) => {
+                if (value && value.match(/[^A-Za-z]/g))
+                  return Promise.reject(new Error('项目简称只能是英文字符'));
+                else return Promise.resolve();
+              },
+            },
+          ]}
+        >
+          <Input maxLength={3} onBlur={onBlurName} />
+        </Form.Item>
+        <Form.Item
+          label="项目期数"
+          name="version"
+          initialValue={data.version}
+          rules={[{ required: true, message: '请选择项目期数' }]}
+        >
+          <Select style={{ width: '100%' }} onChange={changeVersion}>
+            {['一期', '二期', '三期', '四期', '五期'].map((item, index) => (
+              <Option key={index + 1}>{item}</Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item label="项目编号">
+          {codes.type || '***'}-{codes.industry || '***'}-
+          {codes.location || '***'}-{codes.name || '***'}-{codes.version || '*'}
+        </Form.Item>
+      </>
+    );
+  };
+
+  useEffect(() => {
+    if (data.id) {
+      const type = typeList.find((item) => item.id == data.type_id);
+      const industry = industryList.find((item) => item.id == data.industry_id);
+      setCodes({
+        type: type?.code,
+        industry: industry?.code,
+        location: data.location_code,
+        name: data.name,
+        version: data.version,
+      });
+      setType(type);
+    } else {
+      setCodes({
+        type: '',
+        industry: '',
+        location: '',
+        name: '',
+        version: '',
+      });
+      setType({});
+    }
+  }, [data, visible]);
+
+  useEffect(() => {
+    form.resetFields();
+  }, [visible]);
+
+  return (
+    <Modal
+      title="项目立项"
+      confirmLoading={loading}
+      maskClosable={false}
+      destroyOnClose
+      open={visible}
+      onCancel={onClose}
+      onOk={handleOk}
+    >
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label="项目名称"
+          name="project_name"
+          initialValue={String(data.project_name || '')}
+          rules={[{ required: true, message: '请输入项目名称' }]}
+        >
+          <Input style={{ width: '100%' }} />
+        </Form.Item>
+        <Form.Item
+          label="项目类别"
+          name="type_id"
+          initialValue={String(data.type_id || '')}
+          rules={[{ required: true, message: '请选择项目类别' }]}
+        >
+          <Select style={{ width: '100%' }} onChange={changeType}>
+            {typeList.map((item) => (
+              <Option key={item.id}>
+                {item.name}({item.code})
+              </Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item
+          label="流程"
+          name="flow_id"
+          initialValue={String(data.flow_id || '')}
+          rules={[{ required: true, message: '请选择流程' }]}
+        >
+          <Select style={{ width: '100%' }} disabled>
+            {flowList
+              .filter((item) => item && item.id != 2 && item.id != 3)
+              .map((item) => (
+                <Option key={item.id}>{item.name}</Option>
+              ))}
+          </Select>
+        </Form.Item>
+        {currentUser.IsSuper && (
+          <Form.Item
+            label="售前经理"
+            name="author"
+            initialValue={String(
+              data.author && data.author_dep_id
+                ? `${data.author}||${data.author_dep_id}`
+                : '',
+            )}
+          >
+            <TreeSelect
+              showSearch
+              allowClear
+              style={{ width: '100%' }}
+              multiple={false}
+              filterTreeNode={(input, option) => {
+                return option.props.title === input;
+              }}
+              treeData={depUserTree}
+            />
+          </Form.Item>
+        )}
+        {type?.id != 7 && renderDetail()}
+      </Form>
+    </Modal>
+  );
+}
+export default AddModal;

+ 365 - 0
src/pages/PurchaseList/Approval/Auth.js

@@ -0,0 +1,365 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+  Table,
+  Button,
+  Form,
+  Divider,
+  Modal,
+  Popover,
+  Input,
+  Select,
+} from 'antd';
+import moment from 'moment';
+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';
+
+const { Option } = Select;
+//状态
+const STATUS = [
+  {
+    value: 0,
+    label: '售前',
+  },
+  {
+    value: 1,
+    label: '转执行',
+  },
+  {
+    value: 2,
+    label: '转运营',
+  },
+  {
+    value: 3,
+    label: '转质保',
+  },
+];
+
+function Auth(props) {
+  const {
+    initialState: { user },
+  } = useModel('@@initialState');
+  const { industryList, typeList, data, flowList, depRole, dispatch, loading } =
+    props;
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState(false);
+  const [detailVisible, setDetailVisible] = useState(false);
+  const [rejectVisible, setRejectVisible] = useState(false);
+  const [currentItem, setCurrentItem] = useState({});
+  const columns = [
+    {
+      title: '项目编号',
+      dataIndex: 'project_full_code',
+    },
+    {
+      title: '项目名称',
+      dataIndex: 'project_name',
+    },
+    {
+      title: '分类',
+      dataIndex: 'TypeInfo',
+      render: (TypeInfo) =>
+        TypeInfo ? `${TypeInfo.name}(${TypeInfo.code})` : '-',
+    },
+    /*
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '行业',
+      dataIndex: 'IndustryInfo',
+      render: IndustryInfo => `${IndustryInfo.name}(${IndustryInfo.code})`,
+    },
+    {
+      title: '所在地',
+      dataIndex: 'location',
+      render: (location, record) => `${location}(${record.location_code})`,
+    },
+    {
+      title: '期数',
+      dataIndex: 'version',
+      render: version => `${version}期`,
+    },
+    */
+    {
+      title: '流程',
+      dataIndex: ['FlowInfo', 'name'],
+    },
+    {
+      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: 'NodeInfo',
+      render: (nodeInfo, item) => {
+        let statusDom;
+        switch (item.audit_status) {
+          case 0:
+            statusDom = '待提交';
+            break;
+          case 1:
+            statusDom = <span style={{ color: '#1890ff' }}>审核中</span>;
+            break;
+          case 2:
+            statusDom = (
+              <Popover content={`拒绝原因: ${item.audit_comment}`}>
+                <span style={{ color: '#f5222d' }}>审核拒绝</span>
+              </Popover>
+            );
+            break;
+          case 3:
+            statusDom = <span style={{ color: '#a0d911' }}>审核通过</span>;
+            break;
+        }
+        return (
+          <>
+            {nodeInfo.node}({statusDom})
+          </>
+        );
+      },
+    },
+    {
+      title: '售前项目经理',
+      dataIndex: 'AuthorUser',
+      render: (AuthorUser) => (AuthorUser ? AuthorUser.CName : '-'),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'c_time',
+      render: (c_time) => moment(c_time).format('YYYY.MM.DD'),
+    },
+    {
+      title: '执行经理',
+      dataIndex: 'Leader',
+      render: (Leader) => (Leader ? Leader.CName : '-'),
+    },
+    {
+      title: '操作',
+      render: (record) => (
+        <>
+          <a
+            onClick={() => {
+              setCurrentItem(record);
+              setDetailVisible(true);
+            }}
+          >
+            项目详情
+          </a>
+          <Divider type="vertical" />
+          <a
+            onClick={() => {
+              setCurrentItem(record);
+              setVisible(true);
+            }}
+          >
+            审核详情
+          </a>
+        </>
+      ),
+    },
+  ];
+
+  const canAuth = useMemo(() => {
+    let { NodeInfo, audit_status, project_status } = currentItem;
+    if (!NodeInfo || flowList.length == 0 || depRole.length == 0) return;
+    if (audit_status != 1) return;
+    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;
+
+    let { NodeAudits } = flow.Nodes.find((item) => item.id == NodeInfo.id);
+
+    const role = depRole.find((item) => {
+      return NodeAudits.find((audit) => audit.audit_role == item.ID);
+    });
+    console.log(role);
+    return Boolean(role);
+  }, [currentItem, flowList, depRole]);
+
+  const queryList = (page) => {
+    dispatch({
+      type: 'approval/queryAuth',
+      payload: {
+        currentPage: page.current,
+      },
+    });
+  };
+
+  const onAuth = (type) => {
+    if (type == 3) {
+      Modal.confirm({
+        title: '提示',
+        content: '是否确认通过审批?',
+        okText: '通过',
+        cancelText: '取消',
+        onOk() {
+          dispatch({
+            type: 'approval/authApproval',
+            payload: {
+              ...currentItem,
+              audit_status: 3,
+              audit_comment: '',
+            },
+            callback: () => {
+              setVisible(false);
+            },
+          });
+        },
+      });
+    } else {
+      setRejectVisible(true);
+    }
+  };
+
+  const onReject = ({ desc }) => {
+    dispatch({
+      type: 'approval/authApproval',
+      payload: {
+        ...currentItem,
+        audit_status: 2,
+        audit_comment: desc,
+      },
+      callback: () => {
+        setRejectVisible(false);
+        setVisible(false);
+      },
+    });
+  };
+
+  const handleSearch = () => {
+    const { projectName, projectCode, projectStatus } = form.getFieldsValue();
+    // console.log(error,values);
+    let params = {};
+    params.project_name = projectName;
+    params.project_code = projectCode?.toUpperCase();
+    params.project_status = projectStatus;
+
+    dispatch({
+      type: 'approval/queryAuth',
+      payload: params,
+    });
+  };
+
+  const renderSearch = () => {
+    return (
+      <Form
+        style={{ marginBottom: 20 }}
+        layout="inline"
+        initialValues={{
+          projectName: null,
+          projectCode: null,
+          projectStatus: null,
+        }}
+        form={form}
+      >
+        <Form.Item label="项目名称" name="projectName">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="项目编号" name="projectCode">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="状态" name="projectStatus">
+          <Select
+            showSearch
+            style={{ width: 120 }}
+            filterOption={(input, option) =>
+              option.props.children
+                .toLowerCase()
+                .indexOf(input.toLowerCase()) >= 0
+            }
+          >
+            <Option value={null}>全部</Option>
+            {STATUS.map((item) => (
+              <Option key={item.value}>{item.label}</Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  useEffect(() => {
+    dispatch({
+      type: 'approval/queryFlow',
+    });
+    dispatch({
+      type: 'approval/queryAuth',
+    });
+  }, []);
+
+  useEffect(() => {
+    if (!user.ID) return;
+    dispatch({
+      type: 'user/queryDepRole',
+      payload: user,
+    });
+  }, [user]);
+
+  return (
+    <div>
+      {renderSearch()}
+      <Table
+        loading={loading}
+        rowKey="id"
+        dataSource={data.list}
+        pagination={data.pagination}
+        columns={columns}
+        onChange={queryList}
+      />
+      <AuthModal
+        flowList={flowList}
+        visible={visible}
+        data={currentItem}
+        onClose={() => setVisible(false)}
+        onAuth={onAuth}
+        canAuth={canAuth}
+      />
+      <DetailModal
+        flowList={flowList}
+        visible={detailVisible}
+        data={currentItem}
+        onClose={() => setDetailVisible(false)}
+      />
+      <RejectModal
+        visible={rejectVisible}
+        onOk={onReject}
+        onCancel={() => setRejectVisible(false)}
+      />
+    </div>
+  );
+}
+
+export default connect(({ approval, user, loading }) => ({
+  data: approval.auth,
+  typeList: approval.typeList,
+  flowList: approval.flowList,
+  industryList: approval.industryList,
+  depRole: user.depRole,
+  loading: loading.models.approval,
+}))(Auth);

+ 84 - 0
src/pages/PurchaseList/Approval/AuthModal.js

@@ -0,0 +1,84 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { Button, Form, Modal, Steps } from 'antd';
+
+const { Step } = Steps;
+// 新建
+function AuthModal(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) || {};
+  }, [flowList, data]);
+
+  const current = useMemo(() => {
+    if (!data.node_id) return 0;
+    return flow.Nodes?.findIndex((item) => item.id == data.node_id);
+  }, [flowList, data]);
+
+  const getAudits = (nodeInfo) => {
+    switch (nodeInfo.id) {
+      case 11:
+        return '执行项目经理';
+      case 12:
+        return '运营经理';
+      case 13:
+        return '执行项目经理';
+      case 14:
+        return '质保经理';
+      default:
+        return (nodeInfo.NodeAudits || [])
+          .map((item) => item.AuthorRoleInfo.Name)
+          .join(',');
+    }
+  };
+
+  const renderFooter = () => {
+    return (
+      <>
+        <Button onClick={onClose}>取消</Button>
+        {canAuth && (
+          <>
+            <Button onClick={() => onAuth(3)} type="primary">
+              审核通过
+            </Button>
+            <Button onClick={() => onAuth(2)} type="danger">
+              审核拒绝
+            </Button>
+          </>
+        )}
+      </>
+    );
+  };
+
+  return (
+    <Modal
+      title="审核详情"
+      width={800}
+      open={visible}
+      onCancel={onClose}
+      footer={renderFooter()}
+    >
+      <Steps current={current}>
+        {/* <Steps current={data?.node_id}> */}
+        {(flow.Nodes || []).map((item) => (
+          <Step
+            key={item.id}
+            title={item.node}
+            description={`审批人:${getAudits(item)}`}
+          />
+        ))}
+      </Steps>
+    </Modal>
+  );
+}
+export default AuthModal;

+ 85 - 0
src/pages/PurchaseList/Approval/BudgetModal.js

@@ -0,0 +1,85 @@
+import React, { useEffect } from 'react';
+import { Form, Modal, InputNumber } from 'antd';
+import { connect } from 'dva';
+
+function BudgetModal(props) {
+  const { visible, onCancel, onOk, loading, currentItem, dispatch, budget } =
+    props;
+  const [form] = Form.useForm();
+
+  useEffect(() => form.resetFields(), [visible]);
+
+  const subTypeList = [
+    { name: '建设期项目管理人员', code: '02-010', id: 12 },
+    { name: '工程设计人日(含BIM设计)', code: '04-010', id: 13 },
+    { name: '设计联络人日', code: '05-010', id: 14 },
+    { name: '金科陪客户设计联络/培训人日', code: '05-050', id: 16 },
+    { name: '采购和质量控制人日', code: '06-010', id: 17 },
+    { name: '双胞胎运营平台实施人日', code: '07-010', id: 18 },
+    { name: '预算审核及费控人日', code: '08-010', id: 19 },
+    { name: '现场安装/调试/性能测试/试运行人日', code: '10-010', id: 20 },
+    { name: '质保期项目经理人日', code: '11-010', id: 21 },
+    { name: '质保期服务工程师人日', code: '11-030', id: 22 },
+    { name: '运营期项目管理人员', code: '02-030', id: 26 },
+    { name: '运营期培训人日', code: '05-070', id: 27 },
+    { name: '运营期采购和质量控制人日', code: '06-030', id: 28 },
+    { name: '运营期预算审核及费控人日', code: '08-030', id: 29 },
+    { name: '质保采购工程师人日', code: '11-050', id: 30 },
+    { name: '投资技术服务人日', code: '18-010', id: 31 },
+    { name: '运营期技术服务人日', code: '18-030', id: 32 },
+  ];
+
+  const handleOk = () => {
+    form.validateFields().then((values) => {
+      console.log(values);
+      let params = [];
+      let isUpdate = budget?.length !== 0;
+      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;
+        }
+        params.push(elm);
+      });
+      console.log(params);
+      dispatch({
+        type: 'approval/setBudget',
+        payload: params,
+        callback: () => onOk?.(),
+      });
+    });
+  };
+  return (
+    <Modal
+      title="项目预算"
+      open={visible}
+      width={800}
+      onOk={handleOk}
+      onCancel={onCancel}
+      confirmLoading={loading}
+      destroyOnClose
+    >
+      <Form form={form} labelCol={{ span: 10 }} wrapperCol={{ span: 10 }}>
+        {subTypeList.map((item) => (
+          <Form.Item
+            label={item.name}
+            name={item.id}
+            initialValue={
+              Number(
+                budget?.find((child) => child.type_id === item.id)?.workload,
+              ) || 0
+            }
+          >
+            <InputNumber min={0} style={{ width: '100%' }} />
+          </Form.Item>
+        ))}
+      </Form>
+    </Modal>
+  );
+}
+
+export default connect()(BudgetModal);

+ 133 - 0
src/pages/PurchaseList/Approval/DetailModal.js

@@ -0,0 +1,133 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { Form, Modal, Steps } from 'antd';
+import styles from './DetailModal.less';
+
+const { Step } = Steps;
+// 新建
+function DetailModal(props) {
+  const { visible, onClose, onOk, form, data, flowList = [], disabled, loading } = props;
+  const [codes, setCodes] = useState({
+    type: '',
+    industry: '',
+    location: '',
+    name: '',
+    version: '',
+  });
+
+  useEffect(() => {
+    if (!visible || !data.id) return;
+    setCodes({
+      type: data.TypeInfo?.code,
+      industry: data.IndustryInfo?.code,
+      location: data.location_code,
+      name: data.name,
+      version: data.version,
+    });
+  }, [data, visible]);
+
+  const renderDetail = () => (
+    <>
+      <div className={styles.subTitle}>项目详情</div>
+      <Form labelCol={{ span: 4 }} wrapperCol={{ span: 18 }}>
+        <Form.Item className={styles.formItem} label="项目名称">
+          {data.project_name}
+        </Form.Item>
+        {data.TypeInfo && (
+          <Form.Item className={styles.formItem} label="项目类别">
+            {data.TypeInfo?.name}
+          </Form.Item>
+        )}
+        <Form.Item className={styles.formItem} label="流程">
+          {flowList.find(item => item.id == data.flow_id)?.name}
+        </Form.Item>
+        {data.type_id != 7 && (
+          <>
+            <Form.Item className={styles.formItem} label="行业名称">
+              {data.IndustryInfo?.name}
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目地区">
+              {data.location}({data.location_code})
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目简称">
+              {data.name}
+            </Form.Item>
+            <Form.Item className={styles.formItem} label="项目批次">
+              {data.version}期
+            </Form.Item>
+          </>
+        )}
+        {data.AuthorUser && (
+          <Form.Item className={styles.formItem} label="售前项目经理">
+            {data.AuthorUser.CName}
+          </Form.Item>
+        )}
+        {data.AuthorDepInfo && (
+          <Form.Item className={styles.formItem} label="所属部门">
+            {data.AuthorDepInfo.Name}
+          </Form.Item>
+        )}
+        {data.project_full_code && (
+          <Form.Item className={styles.formItem} label="项目编号">
+            {data.project_full_code}
+          </Form.Item>
+        )}
+        {data.WtyManager && (
+          <Form.Item className={styles.formItem} label="质保经理">
+            {data.WtyManager.CName}
+          </Form.Item>
+        )}
+        {data.OptManager && (
+          <Form.Item className={styles.formItem} label="运营经理">
+            {data.OptManager.CName}
+          </Form.Item>
+        )}
+      </Form>
+    </>
+  );
+
+  const flow = useMemo(() => {
+    if (!data.flow_id) return {};
+    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);
+  }, [flowList, data]);
+
+  const getAudits = nodeInfo => {
+    switch (nodeInfo.id) {
+      case 11:
+        return '执行项目经理';
+      case 12:
+        return '运营经理';
+      case 13:
+        return '执行项目经理';
+      case 14:
+        return '质保经理';
+      default:
+        return (nodeInfo.NodeAudits || []).map(item => item.AuthorRoleInfo.Name).join(',');
+    }
+  };
+
+  const renderAuth = () => (
+    <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)}`} />
+        ))}
+      </Steps>
+    </div>
+  );
+
+  return (
+    <Modal title="项目详情" width={800} visible={visible} onCancel={onClose} footer={null}>
+      {/* {data.type_id != 7 && renderDetail()} */}
+      {renderDetail()}
+      {data.audit_status != 0 && renderAuth()}
+    </Modal>
+  );
+}
+export default DetailModal;

+ 16 - 0
src/pages/PurchaseList/Approval/DetailModal.less

@@ -0,0 +1,16 @@
+.authDetail {
+  margin-top: 20px;
+  .auth {
+    padding-top: 10px;
+  }
+}
+
+.subTitle {
+  font-size: 20px;
+  font-weight: bold;
+  margin-bottom: 10px;
+}
+
+.formItem {
+  margin-bottom: 0;
+}

+ 72 - 0
src/pages/PurchaseList/Approval/ExecutionModal.js

@@ -0,0 +1,72 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
+import { connect } from 'dva';
+const { Option } = Select;
+const { TreeNode } = TreeSelect;
+
+function ExecutionModal(props) {
+  const { visible, onOk, onClose, currentItem, loading, depUserTree, dispatch } = props;
+  const [form] = Form.useForm();
+
+  useEffect(() => form.resetFields(), [visible]);
+
+  const handleOk = () => {
+    form.validateFields().then(({ managerID, contractStatus }) => {
+      const [exe_manager_id, dep_id] = managerID.split('||');
+      dispatch({
+        type: 'approval/startExecution',
+        payload: {
+          project_code_id: currentItem.id,
+          with_contract: Number(contractStatus),
+          dep_id: Number(dep_id),
+          exe_manager_id: Number(exe_manager_id),
+        },
+        callback: () => onOk(),
+      });
+    });
+  };
+
+  return (
+    <Modal
+      title="转执行"
+      confirmLoading={loading}
+      maskClosable={false}
+      destroyOnClose
+      visible={visible}
+      onCancel={onClose}
+      onOk={handleOk}
+    >
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label="项目经理"
+          name="managerID"
+          rules={[{ required: true, message: '请选择项目经理' }]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: '100%' }}
+            placeholder="请选择项目经理"
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+            treeData={depUserTree}
+          />
+        </Form.Item>
+
+        <Form.Item
+          label="合同状态"
+          name="contractStatus"
+          rules={[{ required: true, message: '请选择合同状态' }]}
+        >
+          <Select style={{ width: '100%' }}>
+            <Option key={0}>无合同</Option>
+            <Option key={1}>有合同</Option>
+          </Select>
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+}
+export default connect()(ExecutionModal);

+ 614 - 0
src/pages/PurchaseList/Approval/List.js

@@ -0,0 +1,614 @@
+import React, { useState, useEffect } from 'react';
+import {
+  Table,
+  Button,
+  Form,
+  Select,
+  Divider,
+  Modal,
+  Popover,
+  Input,
+  Checkbox,
+} from 'antd';
+import moment from 'moment';
+import styles from './List.less';
+import ApprovalModal from './ApprovalModal';
+import DetailModal from './DetailModal';
+import ExecutionModal from './ExecutionModal';
+import MemberModal from './MemberModal';
+import QualityOperateModal from './QualityOperateModal';
+import BudgetModal from './BudgetModal';
+import ModifyManagerModal from './ModifyManagerModal';
+import { connect } from 'dva';
+import { useRequest, useModel } from '@umijs/max';
+
+const { Option } = Select;
+//状态
+const STATUS = [
+  { value: 0, label: '售前' },
+  { value: 1, label: '转执行' },
+  { value: 2, label: '转运营' },
+  { value: 3, label: '转质保' },
+];
+
+function List(props) {
+  const {
+    initialState: { user },
+  } = useModel('@@initialState');
+  const {
+    industryList,
+    typeList,
+    data,
+    flowList,
+    dispatch,
+    loading,
+    depUserTree,
+    member,
+    budget,
+  } = props;
+  const [form] = Form.useForm();
+  const [addVisible, setAddVisible] = useState(false);
+  const [detailVisible, setDetailVisible] = useState(false);
+  const [executionVisible, setExecutionVisible] = useState(false);
+  const [qualityOperateVisible, setQualityOperateVisible] = useState(false);
+  const [memberVisible, setMemberVisible] = useState(false);
+  const [budgetVisible, setBudgetVisible] = useState(false);
+  const [selfItems, setSelfItems] = useState(false);
+  const [currentItem, setCurrentItem] = useState({});
+  const [qualityOperate, setQualityOperate] = useState(0);
+  const [modifyManagerVisible, setModifyManagerVisible] = useState(false);
+  const columns = [
+    {
+      title: '项目编号',
+      dataIndex: 'project_full_code',
+    },
+    {
+      title: '项目名称',
+      dataIndex: 'project_name',
+    },
+    {
+      title: '分类',
+      dataIndex: 'TypeInfo',
+      render: (TypeInfo) =>
+        TypeInfo ? `${TypeInfo.name}(${TypeInfo.code})` : '-',
+    },
+    /*
+    {
+      title: '名称',
+      dataIndex: 'name',
+    },
+    {
+      title: '行业',
+      dataIndex: 'IndustryInfo',
+      render: IndustryInfo => `${IndustryInfo.name}(${IndustryInfo.code})`,
+    },
+    {
+      title: '所在地',
+      dataIndex: 'location',
+      render: (location, record) => `${location}(${record.location_code})`,
+    },
+    {
+      title: '期数',
+      dataIndex: 'version',
+      render: version => `${version}期`,
+    },
+    */
+    {
+      title: '流程',
+      dataIndex: ['FlowInfo', 'name'],
+    },
+    {
+      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: 'NodeInfo',
+      render: (nodeInfo, item) => {
+        let statusDom;
+        switch (item.audit_status) {
+          case 0:
+            statusDom = '待提交';
+            break;
+          case 1:
+            statusDom = <span style={{ color: '#1890ff' }}>审核中</span>;
+            break;
+          case 2:
+            statusDom = (
+              <Popover content={`拒绝原因: ${item.audit_comment}`}>
+                <span style={{ color: '#f5222d' }}>审核拒绝</span>
+              </Popover>
+            );
+            break;
+          case 3:
+            statusDom = <span style={{ color: '#a0d911' }}>审核通过</span>;
+            break;
+        }
+        return (
+          <>
+            {nodeInfo.node}({statusDom})
+          </>
+        );
+      },
+    },
+    {
+      title: '售前项目经理',
+      dataIndex: 'AuthorUser',
+      render: (AuthorUser) => (AuthorUser ? AuthorUser.CName : '-'),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'c_time',
+      render: (c_time) => moment(c_time).format('YYYY.MM.DD'),
+    },
+    {
+      title: '执行经理',
+      dataIndex: 'Leader',
+      render: (Leader) => (Leader ? Leader.CName : '-'),
+    },
+    {
+      title: '操作',
+      render: (record) => renderEditBtns(record),
+    },
+  ];
+
+  const handleSearch = () => {
+    const { projectName, projectCode, projectStatus } = form.getFieldsValue();
+    let params = {};
+    params.project_name = projectName;
+    params.project_code = projectCode?.toUpperCase();
+    params.project_status = projectStatus;
+    params.currentPage = 1;
+
+    dispatch({
+      type: 'approval/queryApproval',
+      payload: params,
+    });
+  };
+
+  const checkSelf = (e) => {
+    let checked = e.target.checked;
+    setCurrentItem({});
+    setSelfItems(checked);
+    dispatch({
+      type: 'approval/queryApproval',
+      payload: {
+        filter_type: Number(checked),
+        currentPage: 1,
+      },
+    });
+  };
+
+  const renderSearch = () => {
+    return (
+      <Form
+        form={form}
+        layout="inline"
+        initialValues={{
+          projectName: null,
+          projectCode: null,
+          projectStatus: null,
+        }}
+      >
+        <Form.Item label="项目名称" name="projectName">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="项目编号" name="projectCode">
+          <Input style={{ width: 200 }} />
+        </Form.Item>
+        <Form.Item label="状态" name="projectStatus">
+          <Select
+            showSearch
+            style={{ width: 120 }}
+            filterOption={(input, option) =>
+              option.props.children
+                .toLowerCase()
+                .indexOf(input.toLowerCase()) >= 0
+            }
+          >
+            <Option value={null}>全部</Option>
+            {STATUS.map((item) => (
+              <Option key={item.value}>{item.label}</Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  const onOk = (values) => {
+    if (values.id) {
+      dispatch({
+        type: 'approval/updateApproval',
+        payload: values,
+        callback: () => setAddVisible(false),
+      });
+    } else {
+      dispatch({
+        type: 'approval/createApproval',
+        payload: values,
+        callback: () => setAddVisible(false),
+      });
+    }
+  };
+  const onDelete = (item) => {
+    Modal.confirm({
+      title: '删除',
+      content: '是否确认删除该项目',
+      okText: '删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk() {
+        dispatch({
+          type: 'approval/deleteApproval',
+          payload: item,
+        });
+      },
+    });
+  };
+  const onSubmitAuth = (item) => {
+    Modal.confirm({
+      title: '提交审核',
+      content: '是否确认提交审核',
+      okText: '提审',
+      cancelText: '取消',
+      onOk() {
+        dispatch({
+          type: 'approval/submitAudit',
+          payload: {
+            id: item.id,
+            flow_id: item.flow_id,
+            node_id: item.node_id,
+          },
+        });
+      },
+    });
+  };
+  const queryList = (page) => {
+    dispatch({
+      type: 'approval/queryApproval',
+      payload: {
+        currentPage: page.current,
+      },
+    });
+  };
+
+  const renderEditBtns = (record) => {
+    let dividerPush = (item, list) => {
+      if (list.length === 0) list.push(item);
+      else {
+        list.push(<Divider type="vertical" />);
+        list.push(item);
+      }
+    };
+    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>
+    );
+    let editBtn = (
+      <>
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            setAddVisible(true);
+          }}
+        >
+          编辑
+        </a>
+        <Divider type="vertical" />
+        <a
+          onClick={() => {
+            onDelete(record);
+          }}
+        >
+          删除
+        </a>
+        <Divider type="vertical" />
+        <a
+          onClick={() => {
+            onSubmitAuth(record);
+          }}
+        >
+          提交审核
+        </a>
+      </>
+    );
+    let statusBtn = (
+      <>
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            dispatch({
+              type: 'approval/queryBudget',
+              payload: {
+                project_id: record?.id,
+              },
+              callback: () => {
+                setBudgetVisible(true);
+              },
+            });
+          }}
+        >
+          设置人日预算
+        </a>
+        <Divider type="vertical" />
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            setQualityOperateVisible(true);
+            setQualityOperate(0);
+          }}
+        >
+          转质保
+        </a>
+        <Divider type="vertical" />
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            setQualityOperateVisible(true);
+            setQualityOperate(1);
+          }}
+        >
+          转运营
+        </a>
+      </>
+    );
+    let { audit_status, project_status, type_id } = record;
+    //权限审核
+    let canEdit = () => {
+      if (user.IsSuper) return true;
+      switch (audit_status) {
+        case 0:
+          return user.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;
+          return false;
+        case 3:
+          switch (project_status) {
+            case 0:
+              return user.ID == record.author;
+            case 1:
+              return user.ID == record.LeaderId;
+            case 2:
+              return (
+                user.ID == record.LeaderId || user.ID == record.opt_manager_id
+              );
+            case 3:
+              return (
+                user.ID == record.LeaderId || user.ID == record.wty_manager_id
+              );
+          }
+          return false;
+      }
+    };
+    let toReturn = [];
+    dividerPush(detailBtn, toReturn);
+    switch (audit_status) {
+      //未提交
+      case 0:
+        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);
+          dividerPush(statusBtn, toReturn);
+        }
+        break;
+      //审核通过
+      case 3:
+        switch (project_status) {
+          //售前
+          case 0:
+            if (canEdit()) {
+              dividerPush(memberBtn, toReturn);
+              dividerPush(executionBtn, toReturn);
+            }
+            break;
+          //转执行
+          case 1:
+            if (canEdit()) {
+              dividerPush(memberBtn, toReturn);
+              dividerPush(statusBtn, toReturn);
+            }
+            break;
+          //转运营
+          case 2:
+            canEdit() && dividerPush(memberBtn, toReturn);
+            break;
+          //转质保
+          case 3:
+            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);
+    return toReturn;
+  };
+
+  useEffect(() => {
+    dispatch({
+      type: 'approval/queryFlow',
+    });
+    dispatch({
+      type: 'approval/queryType',
+    });
+    dispatch({
+      type: 'approval/queryIndustry',
+    });
+    dispatch({
+      type: 'approval/queryApproval',
+    });
+    dispatch({
+      type: 'approval/fetchDepV2',
+    });
+  }, []);
+
+  return (
+    <div>
+      {renderSearch()}
+      <div className={styles.btns}>
+        <Button
+          onClick={() => {
+            setCurrentItem({});
+            setAddVisible(true);
+          }}
+        >
+          新增项目
+        </Button>
+        <Checkbox checked={selfItems} onChange={checkSelf}>
+          只看自己
+        </Checkbox>
+      </div>
+      <Table
+        loading={loading}
+        rowKey="id"
+        dataSource={data.list}
+        pagination={data.pagination}
+        columns={columns}
+        onChange={queryList}
+      />
+      <ApprovalModal
+        currentUser={user}
+        depUserTree={depUserTree}
+        loading={loading}
+        industryList={industryList}
+        flowList={flowList}
+        typeList={typeList}
+        visible={addVisible}
+        onOk={onOk}
+        data={currentItem}
+        total={data.pagination.total}
+        onClose={() => setAddVisible(false)}
+      />
+      <DetailModal
+        industryList={industryList}
+        flowList={flowList}
+        typeList={typeList}
+        visible={detailVisible}
+        data={currentItem}
+        onClose={() => setDetailVisible(false)}
+      />
+      <ExecutionModal
+        depUserTree={depUserTree}
+        loading={loading}
+        visible={executionVisible}
+        currentItem={currentItem}
+        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}
+        visible={qualityOperateVisible}
+        currentItem={currentItem}
+        onOk={() => setQualityOperateVisible(false)}
+        onClose={() => setQualityOperateVisible(false)}
+        qualityOperate={qualityOperate}
+      />
+      <BudgetModal
+        visible={budgetVisible}
+        loading={loading}
+        currentItem={currentItem}
+        onCancel={() => setBudgetVisible(false)}
+        onOk={() => setBudgetVisible(false)}
+        budget={budget}
+      />
+      <ModifyManagerModal
+        depUserTree={depUserTree}
+        loading={loading}
+        visible={modifyManagerVisible}
+        onClose={() => setModifyManagerVisible(false)}
+        currentItem={currentItem}
+        dataSource={member}
+        onOk={() => setModifyManagerVisible(false)}
+      />
+    </div>
+  );
+}
+
+export default connect(({ approval, user, loading }) => ({
+  data: approval.list,
+  typeList: approval.typeList,
+  flowList: approval.flowList,
+  industryList: approval.industryList,
+  loading: loading.models.approval,
+  depUserTree: approval.depUserTree,
+  member: approval.member,
+  budget: approval.budget,
+}))(List);

+ 10 - 0
src/pages/PurchaseList/Approval/List.less

@@ -0,0 +1,10 @@
+.btns {
+  display: flex;
+  justify-content: space-between;
+  margin: 20px 0;
+  :global {
+    .ant-btn {
+      margin-right: 10px;
+    }
+  }
+}

+ 152 - 0
src/pages/PurchaseList/Approval/MemberModal.js

@@ -0,0 +1,152 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Modal, TreeSelect, Table, Button, Input, Tabs } from 'antd';
+import { connect } from 'dva';
+const { TreeNode } = TreeSelect;
+const { TabPane } = Tabs;
+
+//状态
+const STATUS = [
+  { value: 1, label: '售前' },
+  { value: 2, label: '执行' },
+  { value: 4, label: '运营' },
+  { value: 6, label: '质保' },
+];
+
+function MemberModal(props) {
+  const { visible, onClose, currentItem, loading, depUserTree, dataSource, dispatch } = props;
+  const [type, setType] = useState('1');
+  const [form] = Form.useForm();
+  const [currentMember, setCurrentMember] = useState({});
+
+  const columns = [
+    {
+      title: '工号',
+      dataIndex: 'UserName',
+    },
+    {
+      title: '姓名',
+      dataIndex: 'CName',
+    },
+    {
+      title: '手机号码',
+      dataIndex: 'Mobile',
+    },
+    {
+      title: '操作',
+      //移除按钮,此处应传入当前成员的数据
+      render: member =>
+        member.ID != currentItem.author &&
+        member.ID != currentItem.LeaderId &&
+        member.ID != currentItem.wty_manager_id &&
+        member.ID != currentItem.opt_manager_id && <a onClick={() => onDelete(member)}>移除</a>,
+    },
+  ];
+
+  const onDelete = item => {
+    Modal.confirm({
+      title: '移除成员',
+      content: '是否确认从项目中移除该成员',
+      okText: '移除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk() {
+        dispatch({
+          type: 'approval/deleteMember',
+          payload: { project_code_id: currentItem.id, user_id: item.ID, flow_id: Number(type) },
+        });
+      },
+    });
+  };
+
+  const handleAddMember = () => {
+    form.validateFields().then(({ memberID }) => {
+      dispatch({
+        type: 'approval/addMember',
+        payload: {
+          project_code_id: currentItem.id,
+          user_id: Number(memberID.split('||')[0]),
+          flow_id: Number(type),
+        },
+        callback: () => {
+          form.resetFields();
+        },
+      });
+    });
+    // form.resetFields();
+  };
+
+  const handleChange = type => {
+    setType(type);
+    dispatch({
+      type: 'approval/queryMember',
+      payload: { project_code_id: currentItem.id, flow_id: Number(type) },
+    });
+  };
+
+  useEffect(() => {
+    if (currentItem?.id) {
+      handleChange('1');
+    }
+  }, [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 =>
+            currentItem.flow_id >= item.value && (
+              <TabPane tab={item.label} key={item.value}></TabPane>
+            )
+        )}
+      </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: '请选择成员' }]}
+        >
+          <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);

+ 63 - 0
src/pages/PurchaseList/Approval/ModifyManagerModal.js

@@ -0,0 +1,63 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Modal, TreeSelect, Table, Button, Input } from 'antd';
+import { connect } from 'dva';
+const { TreeNode } = TreeSelect;
+
+function ModifyManagerModal(props) {
+  const { visible, onClose, currentItem, loading, depUserTree, dataSource, dispatch, onOk } = props;
+  const [form] = Form.useForm();
+
+  useEffect(() => {
+    form.resetFields();
+  }, [visible]);
+
+  const handleOk = () => {
+    form.validateFields().then(({ managerID }) => {
+      const [user_id, dep_id] = managerID.split('||');
+      dispatch({
+        type: 'approval/modifyManager',
+        payload: {
+          user_id: Number(user_id),
+          //project_status为0时为售前项目,flag填5
+          //project_status为1时为执行项目,flag填0
+          flag: currentItem?.project_status == 0 ? 5 : 0,
+          project_id: Number(currentItem?.id),
+        },
+        callback: () => onOk?.(),
+      });
+    });
+  };
+
+  return (
+    <Modal
+      title="修改项目经理"
+      confirmLoading={loading}
+      maskClosable={false}
+      destroyOnClose
+      visible={visible}
+      onCancel={onClose}
+      onOk={handleOk}
+    >
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label="项目经理"
+          name="managerID"
+          rules={[{ required: true, message: '请选择项目经理' }]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: '100%' }}
+            placeholder="请选择项目经理"
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+            treeData={depUserTree}
+          />
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+}
+export default connect()(ModifyManagerModal);

+ 75 - 0
src/pages/PurchaseList/Approval/QualityOperateModal.js

@@ -0,0 +1,75 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Select, Modal, Input, TreeSelect } from 'antd';
+import { connect } from 'dva';
+const { Option } = Select;
+const { TreeNode } = TreeSelect;
+
+function QualityOperateModal(props) {
+  const {
+    visible,
+    onOk,
+    onClose,
+    currentItem,
+    loading,
+    depUserTree,
+    dispatch,
+    qualityOperate,
+  } = props;
+  const [form] = Form.useForm();
+
+  useEffect(() => form.resetFields(), [visible]);
+
+  const handleOk = () => {
+    form.validateFields().then(({ managerID }) => {
+      const [manager_id, dep_id] = managerID.split('||');
+      let params = {};
+      params.type = qualityOperate ? 'approval/startOperate' : 'approval/startQuality';
+      params.payload = {
+        project_code_id: currentItem.id,
+        dep_id: Number(dep_id),
+      };
+      qualityOperate
+        ? (params.payload.opt_manager_id = Number(manager_id))
+        : (params.payload.wty_manager_id = Number(manager_id));
+      dispatch({
+        ...params,
+        callback: () => onOk(),
+      });
+    });
+  };
+
+  return (
+    <Modal
+      title={qualityOperate ? '转运营' : '转质保'}
+      confirmLoading={loading}
+      maskClosable={false}
+      destroyOnClose
+      visible={visible}
+      onCancel={onClose}
+      onOk={handleOk}
+    >
+      <Form labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} form={form}>
+        <Form.Item
+          label={qualityOperate ? '运营经理' : '质保经理'}
+          name="managerID"
+          rules={[
+            { required: true, message: qualityOperate ? '请选择运营经理' : '请选择质保经理' },
+          ]}
+        >
+          <TreeSelect
+            showSearch
+            allowClear
+            style={{ width: '100%' }}
+            placeholder={qualityOperate ? '请选择运营经理' : '请选择质保经理'}
+            multiple={false}
+            filterTreeNode={(input, option) => {
+              return option.props.title === input;
+            }}
+            treeData={depUserTree}
+          ></TreeSelect>
+        </Form.Item>
+      </Form>
+    </Modal>
+  );
+}
+export default connect()(QualityOperateModal);

+ 283 - 0
src/pages/PurchaseList/Approval/models/approval.js

@@ -0,0 +1,283 @@
+import {
+  queryApproval,
+  createApproval,
+  updateApproval,
+  deleteApproval,
+  queryType,
+  queryIndustry,
+  queryFlow,
+  authApproval,
+  queryAuth,
+  submitAudit,
+  queryDepV2,
+  addMember,
+  queryMember,
+  startExecution,
+  startOperate,
+  startQuality,
+  deleteMember,
+  queryBudget,
+  setBudget,
+  modifyManager,
+} from '@/services/approval';
+import { message } from 'antd';
+import moment from 'moment';
+
+function getDepUserTree(data) {
+  data.title = `${data.Name}`;
+  data.key = `dep-${data.ID}`;
+  data.value = `dep-${data.ID}`;
+  data.selectable = false;
+  if (!data.children) data.children = new Array();
+
+  if (data.children) {
+    data.children.forEach(item => {
+      getDepUserTree(item);
+    });
+  }
+
+  if (data.Users && data.Users.length !== 0) {
+    data.Users.forEach(item => {
+      item.title = item.CName;
+      item.key = item.ID + '||' + data.ID;
+      item.value = item.ID + '||' + data.ID;
+      item.selectable = true;
+      item.DepId = data.ID;
+      data.children.push(item);
+    });
+  }
+  return data;
+}
+
+export default {
+  namespace: 'approval',
+  state: {
+    typeList: [],
+    industryList: [],
+    flowList: [],
+    list: {
+      list: [],
+      pagination: {},
+    },
+    auth: {
+      list: [],
+      pagination: {},
+    },
+    filter: {},
+    depUserTree: [],
+    member: [],
+    budget: [],
+  },
+
+  effects: {
+    *fetchDepV2({ payload, callback }, { call, put }) {
+      const response = yield call(queryDepV2, { pageSize: 999999 });
+      if (response) {
+        const depUserTree = response.data.list.map(item => {
+          return getDepUserTree(item);
+        });
+        yield put({
+          type: 'save',
+          payload: { depUserTree },
+        });
+      }
+    },
+
+    *queryType({ payload }, { call, put, select }) {
+      const { typeList } = yield select(s => s.approval);
+      if (typeList.length > 0) return;
+      const { data } = yield call(queryType);
+      if (!data) return;
+      yield put({
+        type: 'save',
+        payload: { typeList: data },
+      });
+    },
+    *queryFlow({ payload }, { call, put, select }) {
+      const { flowList } = yield select(s => s.approval);
+      if (flowList.length > 0) return;
+      const { data } = yield call(queryFlow);
+      if (!data) return;
+      yield put({
+        type: 'save',
+        payload: { flowList: data },
+      });
+    },
+    *queryIndustry({ payload }, { call, put, select }) {
+      const { industryList } = yield select(s => s.approval);
+      if (industryList.length > 0) return;
+      const { data } = yield call(queryIndustry);
+      if (!data) return;
+      yield put({
+        type: 'save',
+        payload: { industryList: data },
+      });
+    },
+    *queryApproval({ payload = {}, callback }, { call, put, select }) {
+      const { filter } = yield select(s => s.approval);
+      const newFileter = {
+        ...filter,
+        ...payload,
+      };
+      const { data } = yield call(queryApproval, newFileter);
+      if (!data) return;
+      callback && callback(data);
+      yield put({
+        type: 'save',
+        payload: { list: data, filter: newFileter },
+      });
+    },
+    *queryAuth({ payload = {} }, { call, put }) {
+      const { data } = yield call(queryAuth, payload);
+      if (!data) return;
+      yield put({
+        type: 'save',
+        payload: { auth: data },
+      });
+    },
+    *queryMember({ payload }, { call, put }) {
+      const { data } = yield call(queryMember, payload);
+      if (!data) return;
+      yield put({
+        type: 'save',
+        payload: { member: data.map(item => item.User) },
+      });
+    },
+    *authApproval({ payload, callback }, { call, put }) {
+      const { data } = yield call(authApproval, {
+        id: payload.id,
+        project_full_code: payload.project_full_code,
+        flow_id: payload.flow_id,
+        node_id: payload.node_id,
+        audit_status: payload.audit_status,
+        audit_comment: payload.audit_comment,
+      });
+      if (!data) return;
+      callback && callback();
+      message.success('审核成功');
+      yield put({
+        type: 'queryAuth',
+      });
+    },
+    *createApproval({ payload, callback }, { call, put }) {
+      const res = yield call(createApproval, payload);
+      if (res) {
+        const { data } = res;
+        callback && callback();
+        message.success('创建成功');
+        yield put({
+          type: 'queryApproval',
+        });
+      }
+    },
+    *updateApproval({ payload, callback }, { call, put }) {
+      const { data } = yield call(updateApproval, payload);
+      if (!data) return;
+      callback && callback();
+      message.success('更新成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *deleteApproval({ payload, callback }, { call, put }) {
+      const { data } = yield call(deleteApproval, payload);
+      if (!data) return;
+      callback && callback();
+      message.success('删除成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *submitAudit({ payload, callback }, { call, put }) {
+      const { data } = yield call(submitAudit, payload);
+      if (!data) return;
+      callback && callback();
+      message.success('提审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *addMember({ payload, callback }, { call, put }) {
+      const res = yield call(addMember, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('添加成功');
+      yield put({
+        type: 'queryMember',
+        payload: { project_code_id: payload.project_code_id, flow_id: payload.flow_id },
+      });
+    },
+    *deleteMember({ payload, callback }, { call, put }) {
+      const res = yield call(deleteMember, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('移除成功');
+      yield put({
+        type: 'queryMember',
+        payload: { project_code_id: payload.project_code_id, flow_id: payload.flow_id },
+      });
+    },
+    *startExecution({ payload, callback }, { call, put }) {
+      const res = yield call(startExecution, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('转执行送审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *startOperate({ payload, callback }, { call, put }) {
+      const res = yield call(startOperate, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('转运营送审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+    *startQuality({ payload, callback }, { call, put }) {
+      const res = yield call(startQuality, payload);
+      if (!res) return;
+      callback && callback();
+      message.success('转质保送审成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+
+    *queryBudget({ payload, callback }, { call, put }) {
+      const res = yield call(queryBudget, payload);
+      if (!res) return;
+      yield put({
+        type: 'save',
+        payload: { budget: res.data },
+      });
+      callback?.();
+    },
+
+    *setBudget({ payload, callback }, { call, put }) {
+      const res = yield call(setBudget, payload);
+      if (!res) return;
+      message.success('项目预算设置成功');
+      callback?.();
+    },
+    *modifyManager({ payload, callback }, { call, put }) {
+      const res = yield call(modifyManager, payload);
+      if (!res) return;
+      callback?.();
+      message.success('修改项目经理成功');
+      yield put({
+        type: 'queryApproval',
+      });
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 497 - 0
src/pages/PurchaseList/Approval/provinces.js

@@ -0,0 +1,497 @@
+export default [
+  { code: '010', name: '北京' },
+  { code: '021', name: '上海' },
+  { code: '022', name: '天津' },
+  { code: '023', name: '重庆' },
+  {
+    name: '安徽',
+    children: [
+      { code: '0551', name: '合肥' },
+      { code: '0553', name: '芜湖' },
+      { code: '0556', name: '安庆' },
+      { code: '0552', name: '蚌埠' },
+      { code: '0558', name: '亳州' },
+      { code: '0565', name: '巢湖' },
+      { code: '0566', name: '池州' },
+      { code: '0550', name: '滁州' },
+      { code: '0558', name: '阜阳' },
+      { code: '0559', name: '黄山' },
+      { code: '0561', name: '淮北' },
+      { code: '0554', name: '淮南' },
+      { code: '0564', name: '六安' },
+      { code: '0555', name: '马鞍山' },
+      { code: '0557', name: '宿州' },
+      { code: '0562', name: '铜陵' },
+      { code: '0563', name: '宣城' },
+    ],
+  },
+  {
+    name: '福建',
+    children: [
+      { code: '0591', name: '福州' },
+      { code: '0592', name: '厦门' },
+      { code: '0595', name: '泉州' },
+      { code: '0597', name: '龙岩' },
+      { code: '0593', name: '宁德' },
+      { code: '0599', name: '南平' },
+      { code: '0594', name: '莆田' },
+      { code: '0598', name: '三明' },
+      { code: '0596', name: '漳州' },
+    ],
+  },
+  {
+    name: '甘肃',
+    children: [
+      { code: '0931', name: '兰州' },
+      { code: '0943', name: '白银' },
+      { code: '0932', name: '定西' },
+      { code: '0935', name: '金昌' },
+      { code: '0937', name: '酒泉' },
+      { code: '0933', name: '平凉' },
+      { code: '0934', name: '庆阳' },
+      { code: '0935', name: '武威' },
+      { code: '0938', name: '天水' },
+      { code: '0936', name: '张掖' },
+      { code: '0941', name: '甘南' },
+      { code: '0937', name: '嘉峪关' },
+      { code: '0930', name: '临夏' },
+      { code: '0939', name: '陇南' },
+    ],
+  },
+  {
+    name: '广东',
+    children: [
+      { code: '020', name: '广州' },
+      { code: '0755', name: '深圳' },
+      { code: '0756', name: '珠海' },
+      { code: '0769', name: '东莞' },
+      { code: '0757', name: '佛山' },
+      { code: '0752', name: '惠州' },
+      { code: '0750', name: '江门' },
+      { code: '0760', name: '中山' },
+      { code: '0754', name: '汕头' },
+      { code: '0759', name: '湛江' },
+      { code: '0768', name: '潮州' },
+      { code: '0762', name: '河源' },
+      { code: '0663', name: '揭阳' },
+      { code: '0668', name: '茂名' },
+      { code: '0753', name: '梅州' },
+      { code: '0763', name: '清远' },
+      { code: '0751', name: '韶关' },
+      { code: '0660', name: '汕尾' },
+      { code: '0662', name: '阳江' },
+      { code: '0766', name: '云浮' },
+      { code: '0758', name: '肇庆' },
+    ],
+  },
+  {
+    name: '广西',
+    children: [
+      { code: '0771', name: '南宁' },
+      { code: '0779', name: '北海' },
+      { code: '0770', name: '防城港' },
+      { code: '0773', name: '桂林' },
+      { code: '0772', name: '柳州' },
+      { code: '0771', name: '崇左' },
+      { code: '0772', name: '来宾' },
+      { code: '0774', name: '梧州' },
+      { code: '0778', name: '河池' },
+      { code: '0775', name: '玉林' },
+      { code: '0755', name: '贵港' },
+      { code: '0774', name: '贺州' },
+      { code: '0777', name: '钦州' },
+      { code: '0776', name: '百色' },
+    ],
+  },
+  {
+    name: '贵州',
+    children: [
+      { code: '0851', name: '贵阳' },
+      { code: '0851', name: '安顺' },
+      { code: '0851', name: '遵义' },
+      { code: '0858', name: '六盘水' },
+      { code: '0857', name: '毕节' },
+      { code: '0855', name: '黔东南' },
+      { code: '0859', name: '黔西南' },
+      { code: '0854', name: '黔南' },
+      { code: '0856', name: '铜仁' },
+    ],
+  },
+  {
+    name: '海南',
+    children: [
+      { code: '0898', name: '海口' },
+      { code: '0899', name: '三亚' },
+      { code: '0802', name: '白沙县' },
+      { code: '0801', name: '保亭县' },
+      { code: '0803', name: '昌江县' },
+      { code: '0804', name: '澄迈县' },
+      { code: '0806', name: '定安县' },
+      { code: '0807', name: '东方' },
+      { code: '0898', name: '乐东县' },
+      { code: '0898', name: '临高县' },
+      { code: '0809', name: '陵水县' },
+      { code: '0898', name: '琼海' },
+      { code: '0898', name: '琼中县' },
+      { code: '0898', name: '屯昌县' },
+      { code: '0898', name: '万宁' },
+      { code: '0898', name: '文昌' },
+      { code: '0898', name: '五指山' },
+      { code: '0805', name: '儋州' },
+    ],
+  },
+  {
+    name: '河北',
+    children: [
+      { code: '0311', name: '石家庄' },
+      { code: '0312', name: '保定' },
+      { code: '0314', name: '承德' },
+      { code: '0310', name: '邯郸' },
+      { code: '0315', name: '唐山' },
+      { code: '0335', name: '秦皇岛' },
+      { code: '0317', name: '沧州' },
+      { code: '0318', name: '衡水' },
+      { code: '0316', name: '廊坊' },
+      { code: '0319', name: '邢台' },
+      { code: '0313', name: '张家口' },
+    ],
+  },
+  {
+    name: '河南',
+    children: [
+      { code: '0371', name: '郑州' },
+      { code: '0379', name: '洛阳' },
+      { code: '0378', name: '开封' },
+      { code: '0374', name: '许昌' },
+      { code: '0372', name: '安阳' },
+      { code: '0375', name: '平顶山' },
+      { code: '0392', name: '鹤壁' },
+      { code: '0391', name: '焦作' },
+      { code: '0391', name: '济源' },
+      { code: '0395', name: '漯河' },
+      { code: '0377', name: '南阳' },
+      { code: '0393', name: '濮阳' },
+      { code: '0398', name: '三门峡' },
+      { code: '0370', name: '商丘' },
+      { code: '0373', name: '新乡' },
+      { code: '0376', name: '信阳' },
+      { code: '0396', name: '驻马店' },
+      { code: '0394', name: '周口' },
+    ],
+  },
+  {
+    name: '黑龙江',
+    children: [
+      { code: '0451', name: '哈尔滨' },
+      { code: '0459', name: '大庆' },
+      { code: '0452', name: '齐齐哈尔' },
+      { code: '0454', name: '佳木斯' },
+      { code: '0457', name: '大兴安岭' },
+      { code: '0456', name: '黑河' },
+      { code: '0468', name: '鹤岗' },
+      { code: '0467', name: '鸡西' },
+      { code: '0453', name: '牡丹江' },
+      { code: '0464', name: '七台河' },
+      { code: '0455', name: '绥化' },
+      { code: '0469', name: '双鸭山' },
+      { code: '0458', name: '伊春' },
+    ],
+  },
+  {
+    name: '湖北',
+    children: [
+      { code: '027', name: '武汉' },
+      { code: '0710', name: '襄阳' },
+      { code: '0719', name: '十堰' },
+      { code: '0714', name: '黄石' },
+      { code: '0711', name: '鄂州' },
+      { code: '0718', name: '恩施' },
+      { code: '0713', name: '黄冈' },
+      { code: '0716', name: '荆州' },
+      { code: '0724', name: '荆门' },
+      { code: '0722', name: '随州' },
+      { code: '0717', name: '宜昌' },
+      { code: '0728', name: '天门' },
+      { code: '0728', name: '潜江' },
+      { code: '0728', name: '仙桃' },
+      { code: '0712', name: '孝感' },
+      { code: '0715', name: '咸宁' },
+      { code: '0719', name: '神农架' },
+    ],
+  },
+  {
+    name: '湖南',
+    children: [
+      { code: '0731', name: '长沙' },
+      { code: '0730', name: '岳阳' },
+      { code: '0732', name: '湘潭' },
+      { code: '0736', name: '常德' },
+      { code: '0735', name: '郴州' },
+      { code: '0734', name: '衡阳' },
+      { code: '0745', name: '怀化' },
+      { code: '0738', name: '娄底' },
+      { code: '0739', name: '邵阳' },
+      { code: '0737', name: '益阳' },
+      { code: '0746', name: '永州' },
+      { code: '0733', name: '株洲' },
+      { code: '0744', name: '张家界' },
+      { code: '0743', name: '湘西' },
+    ],
+  },
+  {
+    name: '吉林',
+    children: [
+      { code: '0431', name: '长春' },
+      { code: '0432', name: '吉林' },
+      { code: '0433', name: '延边' },
+      { code: '0436', name: '白城' },
+      { code: '0439', name: '白山' },
+      { code: '0437', name: '辽源' },
+      { code: '0434', name: '四平' },
+      { code: '0438', name: '松原' },
+      { code: '0435', name: '通化' },
+    ],
+  },
+  {
+    name: '江苏',
+    children: [
+      { code: '025', name: '南京' },
+      { code: '0512', name: '苏州' },
+      { code: '0519', name: '常州' },
+      { code: '0518', name: '连云港' },
+      { code: '0523', name: '泰州' },
+      { code: '0510', name: '无锡' },
+      { code: '0516', name: '徐州' },
+      { code: '0514', name: '扬州' },
+      { code: '0511', name: '镇江' },
+      { code: '0517', name: '淮安' },
+      { code: '0513', name: '南通' },
+      { code: '0527', name: '宿迁' },
+      { code: '0515', name: '盐城' },
+    ],
+  },
+  {
+    name: '江西',
+    children: [
+      { code: '0791', name: '南昌' },
+      { code: '0797', name: '赣州' },
+      { code: '0792', name: '九江' },
+      { code: '0798', name: '景德镇' },
+      { code: '0796', name: '吉安' },
+      { code: '0799', name: '萍乡' },
+      { code: '0793', name: '上饶' },
+      { code: '0790', name: '新余' },
+      { code: '0795', name: '宜春' },
+      { code: '0701', name: '鹰潭' },
+      { code: '0794', name: '抚州' },
+    ],
+  },
+  {
+    name: '辽宁',
+    children: [
+      { code: '024', name: '沈阳' },
+      { code: '0411', name: '大连' },
+      { code: '0412', name: '鞍山' },
+      { code: '0415', name: '丹东' },
+      { code: '0413', name: '抚顺' },
+      { code: '0416', name: '锦州' },
+      { code: '0417', name: '营口' },
+      { code: '0414', name: '本溪' },
+      { code: '0421', name: '朝阳' },
+      { code: '0418', name: '阜新' },
+      { code: '0429', name: '葫芦岛' },
+      { code: '0419', name: '辽阳' },
+      { code: '0427', name: '盘锦' },
+      { code: '0410', name: '铁岭' },
+    ],
+  },
+  {
+    name: '内蒙古',
+    children: [
+      { code: '0471', name: '呼和浩特' },
+      { code: '0472', name: '包头' },
+      { code: '0476', name: '赤峰' },
+      { code: '0477', name: '鄂尔多斯' },
+      { code: '0474', name: '乌兰察布' },
+      { code: '0473', name: '乌海' },
+      { code: '0482', name: '兴安盟' },
+      { code: '0470', name: '呼伦贝尔' },
+      { code: '0475', name: '通辽' },
+      { code: '0483', name: '阿拉善盟' },
+      { code: '0478', name: '巴彦淖尔' },
+      { code: '0479', name: '锡林郭勒' },
+    ],
+  },
+  {
+    name: '宁夏',
+    children: [
+      { code: '0951', name: '银川' },
+      { code: '0952', name: '石嘴山' },
+      { code: '0954', name: '固原' },
+      { code: '0953', name: '吴忠' },
+      { code: '0955', name: '中卫' },
+    ],
+  },
+  {
+    name: '青海',
+    children: [
+      { code: '0971', name: '西宁' },
+      { code: '0973', name: '黄南' },
+      { code: '0976', name: '玉树' },
+      { code: '0975', name: '果洛' },
+      { code: '0972', name: '海东' },
+      { code: '0977', name: '海西' },
+      { code: '0974', name: '海南' },
+      { code: '0970', name: '海北' },
+    ],
+  },
+  {
+    name: '山东',
+    children: [
+      { code: '0531', name: '济南' },
+      { code: '0532', name: '青岛' },
+      { code: '0631', name: '威海' },
+      { code: '0535', name: '烟台' },
+      { code: '0536', name: '潍坊' },
+      { code: '0538', name: '泰安' },
+      { code: '0543', name: '滨州' },
+      { code: '0534', name: '德州' },
+      { code: '0546', name: '东营' },
+      { code: '0530', name: '菏泽' },
+      { code: '0537', name: '济宁' },
+      { code: '0635', name: '聊城' },
+      { code: '0539', name: '临沂' },
+      { code: '0634', name: '莱芜' },
+      { code: '0633', name: '日照' },
+      { code: '0533', name: '淄博' },
+      { code: '0632', name: '枣庄' },
+    ],
+  },
+  {
+    name: '山西',
+    children: [
+      { code: '0351', name: '太原' },
+      { code: '0355', name: '长治' },
+      { code: '0352', name: '大同' },
+      { code: '0356', name: '晋城' },
+      { code: '0354', name: '晋中' },
+      { code: '0357', name: '临汾' },
+      { code: '0358', name: '吕梁' },
+      { code: '0349', name: '朔州' },
+      { code: '0350', name: '忻州' },
+      { code: '0359', name: '运城' },
+      { code: '0353', name: '阳泉' },
+    ],
+  },
+  {
+    name: '陕西',
+    children: [
+      { code: '029', name: '西安' },
+      { code: '0915', name: '安康' },
+      { code: '0917', name: '宝鸡' },
+      { code: '0916', name: '汉中' },
+      { code: '0914', name: '商洛' },
+      { code: '0919', name: '铜川' },
+      { code: '0913', name: '渭南' },
+      { code: '0910', name: '咸阳' },
+      { code: '0911', name: '延安' },
+      { code: '0912', name: '榆林' },
+    ],
+  },
+  {
+    name: '四川',
+    children: [
+      { code: '028', name: '成都' },
+      { code: '0816', name: '绵阳' },
+      { code: '0832', name: '资阳' },
+      { code: '0827', name: '巴中' },
+      { code: '0838', name: '德阳' },
+      { code: '0818', name: '达州' },
+      { code: '0826', name: '广安' },
+      { code: '0839', name: '广元' },
+      { code: '0833', name: '乐山' },
+      { code: '0830', name: '泸州' },
+      { code: '028', name: '眉山' },
+      { code: '0832', name: '内江' },
+      { code: '0817', name: '南充' },
+      { code: '0812', name: '攀枝花' },
+      { code: '0825', name: '遂宁' },
+      { code: '0831', name: '宜宾' },
+      { code: '0835', name: '雅安' },
+      { code: '0813', name: '自贡' },
+      { code: '0837', name: '阿坝' },
+      { code: '0836', name: '甘孜' },
+      { code: '0834', name: '凉山' },
+    ],
+  },
+  {
+    name: '西藏',
+    children: [
+      { code: '0891', name: '拉萨' },
+      { code: '0897', name: '阿里' },
+      { code: '0895', name: '昌都' },
+      { code: '0894', name: '林芝' },
+      { code: '0896', name: '那曲' },
+      { code: '0893', name: '山南' },
+    ],
+  },
+  {
+    name: '新疆',
+    children: [
+      { code: '0991', name: '乌鲁木齐' },
+      { code: '0993', name: '石河子' },
+      { code: '0995', name: '吐鲁番' },
+      { code: '0999', name: '伊犁' },
+      { code: '0997', name: '阿克苏' },
+      { code: '0906', name: '阿勒泰' },
+      { code: '0996', name: '巴音' },
+      { code: '0909', name: '博尔塔拉' },
+      { code: '0994', name: '昌吉' },
+      { code: '0902', name: '哈密' },
+      { code: '0903', name: '和田' },
+      { code: '0998', name: '喀什' },
+      { code: '0990', name: '克拉玛依' },
+      { code: '0908', name: '克孜勒' },
+      { code: '0901', name: '塔城' },
+    ],
+  },
+  {
+    name: '云南',
+    children: [
+      { code: '0871', name: '昆明' },
+      { code: '0877', name: '玉溪' },
+      { code: '0878', name: '楚雄' },
+      { code: '0872', name: '大理' },
+      { code: '0873', name: '红河' },
+      { code: '0874', name: '曲靖' },
+      { code: '0691', name: '西双版纳' },
+      { code: '0870', name: '昭通' },
+      { code: '0875', name: '保山' },
+      { code: '0692', name: '德宏' },
+      { code: '0887', name: '迪庆' },
+      { code: '0888', name: '丽江' },
+      { code: '0883', name: '临沧' },
+      { code: '0886', name: '怒江' },
+      { code: '0879', name: '普洱' },
+      { code: '0876', name: '文山' },
+    ],
+  },
+  {
+    name: '浙江',
+    children: [
+      { code: '0571', name: '杭州' },
+      { code: '0574', name: '宁波' },
+      { code: '0573', name: '嘉兴' },
+      { code: '0575', name: '绍兴' },
+      { code: '0577', name: '温州' },
+      { code: '0580', name: '舟山' },
+      { code: '0572', name: '湖州' },
+      { code: '0579', name: '金华' },
+      { code: '0578', name: '丽水' },
+      { code: '0576', name: '台州' },
+      { code: '0570', name: '衢州' },
+    ],
+  },
+  { code: '852', name: '香港' },
+  { code: '853', name: '澳门' },
+];

+ 0 - 153
src/pages/PurchaseList/Index.js

@@ -1,153 +0,0 @@
-import React, { useEffect } from 'react';
-import { Layout, Menu } from 'antd';
-import { connect } from 'dva';
-import RightContent from './RightContent';
-import Link from 'umi/link';
-
-const { Header, Content, Footer } = Layout;
-const { SubMenu } = Menu;
-
-// 布局
-function LayoutDetail(props) {
-  const { currentUser, permission } = props;
-  const isAdmin = currentUser.UserName == 'admin';
-  var logoStyle = {
-    color: 'white',
-    fontWeight: 600,
-    fontSize: 20,
-    verticalAlign: 'middle',
-    marginRight: 60,
-    width: 120,
-  };
-  useEffect(() => {
-    // 查询用户信息
-    props.dispatch({
-      type: 'user/fetchCurrent',
-    });
-  }, []);
-  const checkReport = (state) => {
-    if (isAdmin) return true;
-    const manager =
-      currentUser.is_leader || currentUser.is_opt_mgr || currentUser.is_wty_mgr;
-    switch (state) {
-      case 0:
-        return (
-          currentUser.is_accountant ||
-          manager ||
-          permission['func-01-point-works-report']
-        );
-      case 1:
-        return manager || permission['func-01-point-works-report-p'];
-      case 2:
-        return permission['func-01-point-works-report-d'];
-      case 3:
-        return (
-          currentUser.is_accountant ||
-          permission['func-01-point-works-report-p-s']
-        );
-    }
-  };
-  return (
-    <Layout>
-      <Header>
-        <div
-          style={{
-            display: 'flex',
-            height: '100%',
-            justifyContent: 'space-between',
-          }}
-        >
-          <div style={{ display: 'flex', width: '70%' }}>
-            <div style={logoStyle}>金科环境</div>
-            <Menu
-              theme="dark"
-              mode="horizontal"
-              defaultSelectedKeys={[props.location.pathname]}
-              style={{ lineHeight: '64px', width: '100%' }}
-            >
-              <SubMenu key="/home/work-hours" title="工时管理">
-                <Menu.Item key="/home/work-hours">
-                  <Link to="/home/work-hours">上报工时</Link>
-                </Menu.Item>
-                <Menu.Item key="/home/work-hours-auth">
-                  <Link to="/home/work-hours-auth">审批工时</Link>
-                </Menu.Item>
-              </SubMenu>
-
-              <SubMenu key="/home/approval" title="项目立项">
-                <Menu.Item key="/home/approval/list">
-                  <Link to="/home/approval/list">项目列表</Link>
-                </Menu.Item>
-                <Menu.Item key="/home/approval/auth">
-                  <Link to="/home/approval/auth">审核列表</Link>
-                </Menu.Item>
-              </SubMenu>
-
-              {checkReport(0) && (
-                <SubMenu key="/home/report" title="工时报表">
-                  {/* <Menu.Item key="/home/report/resource">
-                  <Link to="/home/report/resource">资源报表</Link>
-                </Menu.Item> */}
-                  {checkReport(1) && (
-                    <Menu.Item key="/home/report/project">
-                      <Link to="/home/report/project">项目报表</Link>
-                    </Menu.Item>
-                  )}
-                  {checkReport(2) && (
-                    <Menu.Item key="/home/report/department">
-                      <Link to="/home/report/department">部门报表</Link>
-                    </Menu.Item>
-                  )}
-                  {checkReport(3) && (
-                    // <Menu.Item key="/home/report/finance">
-                    //   <Link to="/home/report/finance">财务报表</Link>
-                    // </Menu.Item>
-                    <SubMenu key="/home/report/finance" title="财务报表">
-                      <Menu.Item key="/home/report/finance">
-                        <Link to="/home/report/finance">资源总表</Link>
-                      </Menu.Item>
-                      <Menu.Item key="/home/report/finance/resources">
-                        <Link to="/home/report/finance/resources">
-                          资源中心人日使用汇总表
-                        </Link>
-                      </Menu.Item>
-                      <Menu.Item key="/home/report/finance/project">
-                        <Link to="/home/report/finance/project">
-                          执行项目人日汇总表
-                        </Link>
-                      </Menu.Item>
-                    </SubMenu>
-                  )}
-                </SubMenu>
-              )}
-
-              {/* <Menu.Item key="/home/demo" title="demo">
-                <Link to="/home/demo">demo</Link>
-              </Menu.Item> */}
-
-              {/* {isAdmin && (
-              <Menu.Item key="/home">
-                <Link to="/home">采购清单</Link>
-              </Menu.Item>
-              )} */}
-              {/* <Menu.Item key="/home/flow-list">
-                <Link to="/home/flow-list">流程图</Link>
-              </Menu.Item> */}
-            </Menu>
-          </div>
-          <RightContent />
-        </div>
-      </Header>
-      <Content style={{ padding: '0 50px', minHeight: 'calc(100vh - 64px)' }}>
-        <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
-          {props.children}
-        </div>
-      </Content>
-      {/* <Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer> */}
-    </Layout>
-  );
-}
-export default connect(({ user }) => ({
-  currentUser: user.currentUser,
-  permission: user.currentUser.Permission,
-}))(LayoutDetail);

+ 304 - 0
src/pages/PurchaseList/Report/Demo.js

@@ -0,0 +1,304 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Table, Modal, Select } from 'antd';
+import * as echarts from 'echarts';
+import styles from './report.less';
+import { index } from '@antv/x6/lib/util/dom/elem';
+
+function ProjectTable(props) {
+  const { type, expandedTable, setMemberVisible } = props;
+
+  const temp = [
+    { index: 1, name: '项目A' },
+    { index: 2, name: '项目B' },
+    { index: 3, name: '项目C' },
+  ];
+
+  const column0 = [
+    { title: '未提交', render: () => 0 },
+    { title: '已提交', render: () => 0 },
+    { title: '已审批', render: () => 0 },
+    { title: '已拒绝', render: () => 0 },
+    { title: '计入考核范围', render: () => 0 },
+  ];
+
+  const column1 = [
+    { title: '序号', dataIndex: 'index' },
+    {
+      title: '项目名称',
+      dataIndex: 'name',
+      render: text => <a onClick={() => setMemberVisible(true)}>{text}</a>,
+    },
+    { title: '项目编号' },
+    { title: '技术中心', children: column0 },
+    { title: '招采中心', children: column0 },
+    { title: '总计', render: () => 0 },
+  ];
+
+  const column2 = [
+    { title: '序号', dataIndex: 'index' },
+    Table.EXPAND_COLUMN,
+    { title: '项目名称', dataIndex: 'name' },
+    { title: '项目编号' },
+    { title: '本月人日数', render: () => 0 },
+    { title: '累计人日数', render: () => 0 },
+    { title: '项目预算人日数', render: () => '未设置' },
+  ];
+
+  switch (type) {
+    case 1:
+      return (
+        <Table
+          columns={column1}
+          dataSource={temp}
+          pagination={false}
+          rowKey="index"
+          expandable={expandedTable ? { expandedRowRender: () => expandedTable } : null}
+        />
+      );
+    case 2:
+      return (
+        <Table
+          columns={column2}
+          dataSource={temp}
+          pagination={false}
+          rowKey="index"
+          expandable={expandedTable ? { expandedRowRender: () => expandedTable } : null}
+        />
+      );
+  }
+}
+
+function ProjectManagerTable(props) {
+  const { expandedTable } = props;
+  const column = [
+    { title: '序号', dataIndex: 'index' },
+    { title: '项目经理', dataIndex: 'name' },
+    Table.EXPAND_COLUMN,
+    { title: '本月人日数', render: () => 0 },
+    { title: '截止到目前为止的人日数', render: () => 0 },
+    { title: '预算人日数', render: () => 0 },
+  ];
+  const temp = [
+    { index: 1, name: '项目经理A' },
+    { index: 2, name: '项目经理B' },
+    { index: 3, name: '项目经理C' },
+  ];
+  return (
+    <Table
+      columns={column}
+      dataSource={temp}
+      pagination={false}
+      rowKey="index"
+      expandable={expandedTable ? { expandedRowRender: () => expandedTable } : null}
+    />
+  );
+}
+
+function ProjectTypeTable(props) {
+  const { expandedTable } = props;
+
+  const temp = [
+    { type: '建设期项目管理人日' },
+    { type: '运营期项目管理人日' },
+    { type: '工程设计人日(设计联络、工艺、电气自控、机械、审核)' },
+    { type: '培训人日' },
+    { type: '金科陪客户培训人日' },
+    { type: '运营期培训人日' },
+    { type: '采购和质量控制人日' },
+  ];
+
+  const column = [
+    { title: '分项工作代码', dataIndex: 'type' },
+    { title: '本月人日数', render: () => 0 },
+    { title: '累计人日数', render: () => 0 },
+    { title: '项目预算人日数', render: () => <a>设置</a> },
+  ];
+
+  return (
+    <Table
+      columns={column}
+      dataSource={temp}
+      expandable={expandedTable ? { expandedRowRender: () => expandedTable } : null}
+    />
+  );
+}
+
+function MemberModal(props) {
+  const { memberVisible, setMemberVisible } = props;
+
+  const column = [
+    { title: '序号', dataIndex: 'index' },
+    { title: '员工名称', dataIndex: 'name' },
+    { title: '归属部门' },
+    { title: '未提交', render: () => 0 },
+    { title: '已提交', render: () => 0 },
+    { title: '已审批', render: () => 0 },
+    { title: '已拒绝', render: () => 0 },
+    { title: '小计', render: () => 0 },
+  ];
+
+  const temp = [
+    { index: 1, name: '员工A' },
+    { index: 2, name: '员工B' },
+    { index: 3, name: '员工C' },
+  ];
+
+  return (
+    <Modal visible={memberVisible} onCancel={() => setMemberVisible(false)} width="80%">
+      <Table columns={column} dataSource={temp} pagination={false} />
+    </Modal>
+  );
+}
+
+function Demo(props) {
+  const [memberVisible, setMemberVisible] = useState(false);
+
+  var chartRef = useRef(null);
+
+  const expandedRowRender = record => {
+    switch (record.index) {
+      case 1:
+        return <ProjectTable type={1} setMemberVisible={setMemberVisible} />;
+      case 2:
+        return <ProjectTable type={2} expandedTable={<ProjectTypeTable />} />;
+    }
+  };
+
+  const getData = value => {
+    switch (value) {
+      case 1:
+        return [
+          { value: 1048, name: '执行' },
+          { value: 735, name: '售前' },
+          { value: 580, name: '市场品牌' },
+          { value: 484, name: '日常' },
+          { value: 300, name: '标准化' },
+          { value: 520, name: '研发' },
+        ];
+      case 2:
+        return [
+          { value: 345, name: '执行' },
+          { value: 316, name: '售前' },
+          { value: 650, name: '市场品牌' },
+          { value: 484, name: '日常' },
+          { value: 981, name: '标准化' },
+          { value: 489, name: '研发' },
+        ];
+      case 3:
+        return [
+          { value: 987, name: '执行' },
+          { value: 916, name: '售前' },
+          { value: 354, name: '市场品牌' },
+          { value: 451, name: '日常' },
+          { value: 266, name: '标准化' },
+          { value: 781, name: '研发' },
+        ];
+      case 4:
+        return [
+          { value: 519, name: '执行' },
+          { value: 313, name: '售前' },
+          { value: 974, name: '市场品牌' },
+          { value: 597, name: '日常' },
+          { value: 351, name: '标准化' },
+          { value: 259, name: '研发' },
+        ];
+      case 5:
+        return [
+          { value: 149, name: '执行' },
+          { value: 159, name: '售前' },
+          { value: 245, name: '市场品牌' },
+          { value: 518, name: '日常' },
+          { value: 259, name: '标准化' },
+          { value: 678, name: '研发' },
+        ];
+    }
+  };
+
+  const onSelect = value => {
+    chartRef.current.setOption({ series: [{ data: getData(value) }] });
+  };
+
+  useEffect(() => {
+    chartRef.current = echarts.init(document.getElementById('chart'));
+    chartRef.current.setOption({
+      tooltip: {
+        trigger: 'item',
+      },
+      series: [
+        {
+          type: 'pie',
+          radius: '70%',
+          data: getData(1),
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)',
+            },
+          },
+        },
+      ],
+    });
+  }, []);
+
+  const temp = [
+    { index: 1, type: '售前项目' },
+    { index: 2, type: '执行项目' },
+  ];
+  const column0 = [
+    { title: '未提交', render: () => 0 },
+    { title: '已提交', render: () => 0 },
+    { title: '已审批', render: () => 0 },
+    { title: '已拒绝', render: () => 0 },
+    { title: '计入考核范围', render: () => 0 },
+  ];
+  const column1 = [
+    { title: '序号', dataIndex: 'index' },
+    Table.EXPAND_COLUMN,
+    { title: '人日类型', dataIndex: 'type' },
+    { title: '技术中心', children: column0 },
+    { title: '招采中心', children: column0 },
+  ];
+  const select = [
+    { value: 1, key: '部门A' },
+    { value: 2, key: '部门B' },
+    { value: 3, key: '部门C' },
+    { value: 4, key: '部门D' },
+    { value: 5, key: '部门E' },
+  ];
+  return (
+    <>
+      <div>事业部总</div>
+      <Table
+        columns={column1}
+        dataSource={temp}
+        expandable={{
+          expandedRowRender,
+        }}
+        rowKey="index"
+        pagination={false}
+      />
+
+      <div>销售副总/执行副总</div>
+      <ProjectManagerTable expandedTable={<ProjectTable type={2} />} />
+
+      <div>销售项目经理</div>
+      <ProjectTable type={2} />
+
+      <div>执行项目经理</div>
+      <ProjectTable type={2} expandedTable={<ProjectTypeTable />} />
+
+      <Select style={{ width: '100px' }} defaultValue={1} onChange={onSelect}>
+        {select.map(item => (
+          <Select.Option value={item.value} key={item.value}>
+            {item.key}
+          </Select.Option>
+        ))}
+      </Select>
+      <div id="chart" style={{ width: '600px', height: '600px' }}></div>
+      <MemberModal memberVisible={memberVisible} setMemberVisible={setMemberVisible} />
+    </>
+  );
+}
+
+export default Demo;

+ 175 - 0
src/pages/PurchaseList/Report/DepCompareModal.js

@@ -0,0 +1,175 @@
+import React, { useState, useEffect } from 'react';
+import { Modal, Table } from 'antd';
+import { connect } from 'dva';
+import moment from 'moment';
+import { set } from 'lodash';
+
+function DepCompareModal(props) {
+  const {
+    dispatch,
+    visible,
+    onOk,
+    onCancel,
+    depCompare,
+    depUserProject,
+    depUser,
+    filter,
+    loading,
+  } = props;
+  const [userVisible, setUserVisible] = useState(false);
+  //本部门
+  const columns = [
+    { title: '用户名称', dataIndex: 'c_name' },
+    { title: '员工号', dataIndex: 'user_name' },
+    { title: '执行项目人日', dataIndex: 'type_project_cnt' },
+    { title: '售前支持', dataIndex: 'type_sale_cnt' },
+    { title: '市场品牌', dataIndex: 'type_market_cnt' },
+    { title: '日常', dataIndex: 'type_normal_cnt' },
+    { title: '标准化', dataIndex: 'type_standardize_cnt' },
+    { title: '研发', dataIndex: 'type_rd_cnt' },
+    // { title: '漏填工时', dataIndex: 'type_lost_cnt' },
+    { title: '应填报总工时', dataIndex: 'total_cnt' },
+    {
+      title: '有效利用率',
+      dataIndex: 'usage_percent',
+      render: (percent = 0) => (percent * 100).toFixed(2) + '%',
+    },
+  ];
+  // 其他部门
+  const columnsDep = [
+    {
+      width: 350,
+      render: record =>
+        // record.dep_name || <a onClick={() => onClickUser(record)}>{record.c_name}</a>,
+        record.dep_name || record.c_name,
+    },
+    { title: '执行项目', dataIndex: 'type_project_cnt' },
+    { title: '售前项目', dataIndex: 'type_sale_cnt' },
+    { title: '研发项目', dataIndex: 'type_rd_cnt' },
+    { title: '运营项目', dataIndex: 'type_opt_cnt' },
+    { title: '质保项目', dataIndex: 'type_wty_cnt' },
+    { title: '总计', dataIndex: 'total_cnt' },
+  ];
+  const columnsUser = [
+    { title: '项目名称', dataIndex: 'project_name' },
+    { title: '总工时', dataIndex: 'total_cnt' },
+  ];
+
+  const onChangePage = pagination => {
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: {
+        ...filter,
+        currentPage: pagination.current,
+      },
+    });
+  };
+
+  const onExpand = (expanded, record) => {
+    if (expanded && !record.isLoad) {
+      dispatch({
+        type: 'report/queryDepCompareUser',
+        payload: {
+          s_time: filter.s_time,
+          e_time: filter.e_time,
+          project_dep_id: filter.dep_id,
+          record: record,
+        },
+      });
+    }
+  };
+
+  const onClickUser = user => {
+    dispatch({
+      type: 'report/queryDepUserProject',
+      payload: {
+        s_time: filter.s_time,
+        e_time: filter.e_time,
+        project_dep_id: filter.dep_id,
+        dep_id: user.dep_id,
+        user_id: user.user_id,
+      },
+    });
+    setUserVisible(true);
+  };
+
+  useEffect(() => {
+    if (!filter?.dep_id) return;
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: filter,
+    });
+    // dispatch({
+    //   type: 'report/queryDepCompare',
+    //   payload: filter,
+    // });
+  }, [filter]);
+
+  return (
+    <>
+      <Modal
+        title="部门对账单"
+        width="80%"
+        visible={visible}
+        onCancel={onCancel}
+        footer={false}
+        destroyOnClose
+      >
+        <Table
+          title={() => '本部门工时详情'}
+          columns={columns}
+          loading={loading}
+          dataSource={depUser.list}
+          pagination={depUser.pagination}
+          onChange={onChangePage}
+        ></Table>
+        {/* <Table
+          title={() => '本部门在所属项目下产生的工时'}
+          columns={columns}
+          loading={loading}
+          dataSource={depCompare.length == 0 ? [] : [depCompare[0]]}
+          rowKey="key"
+          childrenColumnName="child"
+          pagination={false}
+          onExpand={onExpand}
+        /> */}
+        {/* {depCompare.length > 0 && (
+          <Table
+            title={() => '其他部门在所属项目下产生的工时'}
+            columns={columnsDep}
+            loading={loading}
+            dataSource={depCompare}
+            rowKey="key"
+            childrenColumnName="child"
+            pagination={false}
+            onExpand={onExpand}
+          />
+        )} */}
+      </Modal>
+
+      {/* <Modal
+        title="用户详情"
+        width="80%"
+        visible={userVisible}
+        onCancel={() => setUserVisible(false)}
+        footer={false}
+        destroyOnClose
+      >
+        <Table
+          columns={columnsUser}
+          loading={loading}
+          dataSource={depUserProject}
+          pagination={false}
+          rowKey="project_name"
+        ></Table>
+      </Modal> */}
+    </>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  depCompare: report.depCompare,
+  depUserProject: report.depUserProject,
+  depUser: report.depUser,
+  loading: loading.models.report,
+}))(DepCompareModal);

+ 363 - 0
src/pages/PurchaseList/Report/Department.js

@@ -0,0 +1,363 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { connect } from 'dva';
+import {
+  Form,
+  Table,
+  DatePicker,
+  Input,
+  Button,
+  Empty,
+  Card,
+  Affix,
+  TreeSelect,
+} from 'antd';
+import styles from './report.less';
+import UserRptModal from './UserRptModal';
+import DepCompareModal from './DepCompareModal';
+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
+var initDate;
+if (dayjs().date() > 25) {
+  initDate = [
+    dayjs().subtract(1, 'month').set('date', 26),
+    dayjs().set('date', 25),
+  ];
+} else {
+  initDate = [
+    dayjs().subtract(2, 'month').set('date', 26),
+    dayjs().subtract(1, 'month').set('date', 25),
+  ];
+}
+
+function Department(props) {
+  const { dispatch, loading, dep } = props;
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState(false);
+  const [modalFilter, setModalFilter] = useState({});
+  const [current, setCurrent] = useState(null);
+  const [currentDep, setCurrentDep] = useState(null);
+  const chartRef = useRef(null);
+  const filterRef = useRef({ pageSize: 99999 });
+  //控制图表部门选择参数
+  const [loadedDepKey, setLoadedDepKey] = useState([]);
+  const [expandedDepKey, setExpandedDepKey] = useState([]);
+
+  const columns = [
+    {
+      title: '部门名称',
+      render: (record) => (
+        <a onClick={() => showUserModal(record)}>{record.dep_name}</a>
+      ),
+      // render: record => <a onClick={() => setCurrent(record)}>{record.dep_name}</a>,
+      width: '32%',
+    },
+    {
+      title: '有效利用率',
+      dataIndex: 'usage_percent',
+      render: (percent) => (percent * 100).toFixed(2) + '%',
+    },
+    {
+      title: '执行项目人日',
+      dataIndex: 'type_project_cnt',
+    },
+    {
+      title: '售前支持',
+      dataIndex: 'type_sale_cnt',
+    },
+    {
+      title: '市场品牌',
+      dataIndex: 'type_market_cnt',
+    },
+    {
+      title: '日常',
+      dataIndex: 'type_normal_cnt',
+    },
+    {
+      title: '标准化',
+      dataIndex: 'type_standardize_cnt',
+    },
+    {
+      title: '研发',
+      dataIndex: 'type_rd_cnt',
+    },
+    // {
+    //   title: '漏填工时',
+    //   dataIndex: 'type_lost_cnt',
+    // },
+    {
+      title: '应填报总工时',
+      dataIndex: 'total_cnt',
+    },
+    // {
+    //   title: '操作',
+    //   width: 80,
+    //   render: item => <a onClick={() => showDepCompare(item)}>详情</a>,
+    // },
+    // {
+    //   title: '付费工时数',
+    //   dataIndex: 'pay_workload_cnt',
+    // },
+    // {
+    //   title: '付费工时率',
+    //   dataIndex: 'pay_workload_percent',
+    //   render: percent => (percent * 100).toFixed(2) + '%',
+    // },
+  ];
+  const handleSearch = () => {
+    const { time } = form.getFieldsValue();
+    filterRef.current.s_time = time[0]
+      ? dayjs(time[0]).format('YYYY-MM-DD')
+      : '';
+    filterRef.current.e_time = time[1]
+      ? dayjs(time[1]).format('YYYY-MM-DD')
+      : '';
+
+    //重置图表部门选择
+    setLoadedDepKey([]);
+    setExpandedDepKey([]);
+
+    dispatch({
+      type: 'report/queryDepReport',
+      payload: {
+        filter: filterRef.current,
+      },
+      callback: (list) => handleChangeCurrent(list[0]),
+    });
+  };
+  const handleDownload = (finance) => {
+    const token = getToken();
+    const { time } = form.getFieldsValue();
+    let s_time = time[0] ? dayjs(time[0]).format('YYYY-MM-DD') : '';
+    let e_time = time[1] ? dayjs(time[1]).format('YYYY-MM-DD') : '';
+
+    if (finance)
+      downloadFile(
+        `/api/v2/workload/finance/people/export?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`,
+        `财务报表_部门${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
+      );
+    else
+      downloadFile(
+        `/api/v2/workload/rpt/dep/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`,
+        `部门报表${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
+      );
+  };
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  //控制图表部门选择展开/加载
+  const onExpandDep = (keys) => {
+    setExpandedDepKey(keys);
+  };
+  const onLoadDep = (expanded, record) => {
+    return new Promise((resolve) => {
+      if (expanded && !record.isLoad) {
+        dispatch({
+          type: 'report/queryDepReport',
+          payload: {
+            filter: filterRef.current,
+            record: record,
+          },
+          callback: resolve,
+        });
+      } else {
+        resolve();
+      }
+    });
+  };
+
+  const showUserModal = (item) => {
+    // const showDepCompare = item => {
+    const { s_time, e_time } = filterRef.current;
+    setModalFilter({
+      s_time: s_time,
+      e_time: e_time,
+      dep_id: item.dep_id,
+    });
+    setVisible(true);
+  };
+  const renderChart = () => {
+    current;
+    chartRef.current.setOption({
+      tooltip: {
+        trigger: 'item',
+      },
+      graphic: {
+        type: 'text',
+        left: 'center',
+        top: 'center',
+        style: {
+          text: `有效利用率\n${
+            current.usage_percent
+              ? (current.usage_percent * 100).toFixed(2)
+              : '-'
+          }%`,
+          textAlign: 'center',
+        },
+      },
+      series: [
+        {
+          type: 'pie',
+          radius: ['40%', '70%'],
+          data: current.data,
+          hoverAnimation: false,
+          itemStyle: { shadowColor: 'rgba(0, 0, 0, 0.5)', shadowBlur: 5 },
+        },
+      ],
+    });
+  };
+
+  const handleChangeCurrent = (item) => {
+    let data = [
+      { value: item.type_project_cnt, name: '执行项目人日' },
+      { value: item.type_sale_cnt, name: '售前支持' },
+      { value: item.type_market_cnt, name: '市场品牌' },
+      { value: item.type_normal_cnt, name: '日常' },
+      { value: item.type_standardize_cnt, name: '标准化' },
+      { value: item.type_rd_cnt, name: '研发' },
+    ];
+    setCurrentDep(item.dep_id);
+    // 过滤为0的值
+    data = data.filter((item) => item.value);
+    if (data.length > 0) {
+      data.push({
+        value: item.type_lost_cnt,
+        name: '漏填工时',
+        tooltip: {
+          backgroundColor: 'transparent',
+          formatter: () => ' ',
+        },
+        itemStyle: { color: '#fff' },
+        emphasis: {
+          label: { show: false },
+          labelLine: { show: false },
+          itemStyle: { color: '#fff' },
+        },
+        label: { show: false },
+        labelLine: { show: false },
+        selected: false,
+      });
+      setCurrent({ data, usage_percent: item.usage_percent });
+    } else {
+      setCurrent(null);
+    }
+  };
+
+  const renderDepSelect = () => {
+    return (
+      <TreeSelect
+        showSearch
+        allowClear
+        placeholder="请选择部门"
+        style={{ width: '80%' }}
+        multiple={false}
+        treeData={dep.list}
+        fieldNames={{
+          label: 'dep_name',
+          value: 'dep_id',
+        }}
+        filterTreeNode={(input, option) => {
+          return option.props.dep_name.includes(input);
+        }}
+        onSelect={(_, node) => {
+          handleChangeCurrent(node);
+        }}
+        treeExpandedKeys={expandedDepKey}
+        onTreeExpand={(keys) => onExpandDep(keys)}
+        treeLoadedKeys={loadedDepKey}
+        loadData={(node) => onLoadDep(true, node)}
+        value={currentDep}
+      />
+    );
+  };
+
+  useEffect(() => {
+    // dispatch({
+    //   type: 'report/queryUserReport',
+    // });
+    handleSearch();
+    chartRef.current = echarts.init(document.getElementById('chart'));
+  }, []);
+
+  useEffect(() => {
+    if (current) {
+      renderChart();
+    }
+  }, [current]);
+
+  return (
+    <div>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <div>
+          <Button
+            type="primary"
+            onClick={() => handleDownload(1)}
+            style={{ marginRight: '10px' }}
+          >
+            财务报表导出
+          </Button>
+          <Button type="primary" onClick={() => handleDownload(0)}>
+            导出
+          </Button>
+        </div>
+      </div>
+      <div style={{ marginTop: 20, display: 'flex' }}>
+        <Table
+          loading={loading}
+          rowKey="dep_id"
+          style={{ width: '100%' }}
+          columns={columns}
+          dataSource={dep.list}
+          pagination={false}
+          onExpand={onLoadDep}
+        />
+        <Affix offsetTop={20}>
+          <Card
+            // extra={<CloseOutlined onClick={() => setCurrent(null)} />}
+            title={renderDepSelect()}
+            style={{ display: 'block', marginLeft: 20 }}
+          >
+            {!current && <Empty style={{ width: 400 }} />}
+            <div
+              id="chart"
+              style={{
+                width: 400,
+                height: 340,
+                display: current ? 'block' : 'none',
+              }}
+            ></div>
+          </Card>
+        </Affix>
+      </div>
+
+      {/* <UserRptModal filter={modalFilter} visible={visible} onCancel={() => setVisible(false)} /> */}
+      <DepCompareModal
+        filter={modalFilter}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      />
+    </div>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  dep: report.dep,
+  loading: loading.models.report,
+}))(Department);

+ 159 - 0
src/pages/PurchaseList/Report/Finance.js

@@ -0,0 +1,159 @@
+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';
+
+const { RangePicker } = DatePicker;
+
+function Finance(props) {
+  const { dispatch, loading, finance, userList } = props;
+  const [form] = Form.useForm();
+  const [modalFilter, setModalFilter] = useState({});
+
+  var initDate;
+  if (dayjs().date() > 25) {
+    initDate = [
+      dayjs().subtract(1, 'month').set('date', 26),
+      dayjs().set('date', 25),
+    ];
+  } else {
+    initDate = [
+      dayjs().subtract(2, 'month').set('date', 26),
+      dayjs().subtract(1, 'month').set('date', 25),
+    ];
+  }
+
+  const columns = [
+    { title: '资源名称', dataIndex: 'c_name' },
+    { title: '资源编号', dataIndex: 'user_name' },
+    { title: '资源归属部门', dataIndex: 'dep_name' },
+    { title: '项目名称', dataIndex: 'project_name' },
+    { title: '项目编号', dataIndex: 'project_code' },
+    { title: '项目状态', dataIndex: 'project_status' },
+    { title: '项目归属部门', dataIndex: 'project_dep_name' },
+    { title: '有效工时', dataIndex: 'workload' },
+    { title: '工时类型', dataIndex: 'type_name' },
+  ];
+
+  const STATUS = [
+    { value: 0, label: '售前' },
+    { value: 1, label: '转执行' },
+    { value: 2, label: '转运营' },
+    { value: 3, label: '转质保' },
+  ];
+
+  const filterRef = useRef({ pageSize: 20 });
+
+  const onChangePage = (pagination) => {
+    dispatch({
+      type: 'report/queryFinanceReport',
+      payload: {
+        ...filterRef.current,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    const { time, user_id, project_name, project_status } =
+      form.getFieldsValue();
+    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;
+    filterRef.current.user_id = user_id;
+    filterRef.current.project_name = project_name;
+    filterRef.current.project_status = project_status;
+
+    dispatch({
+      type: 'report/queryFinanceReport',
+      payload: {
+        ...filterRef.current,
+      },
+    });
+  };
+  const handleDownload = () => {
+    const token = getToken();
+    const s_time = filterRef.current?.s_time || '';
+    const e_time = filterRef.current?.e_time || '';
+    const user_id = filterRef.current?.user_id || '';
+    const project_name = filterRef.current?.project_name || '';
+    const project_status = filterRef.current?.project_status || '';
+    downloadFile(
+      `/api/v2/workload/finance/export2excel?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}&user_id=${user_id}&project_name=${project_name}&project_status${project_status}`,
+      `财务报表${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
+    );
+  };
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item label="资源名称" name="user_id">
+          <Select
+            options={userList}
+            fieldNames={{ label: 'CName', value: 'ID' }}
+            style={{ width: 150 }}
+            showSearch
+            filterOption={(inputValue, option) =>
+              option.CName.includes(inputValue)
+            }
+            allowClear
+          />
+        </Form.Item>
+        <Form.Item label="项目名称" name="project_name">
+          <Input />
+        </Form.Item>
+        <Form.Item label="项目状态" name="project_status" initialValue={null}>
+          <Select style={{ width: 120 }}>
+            <Select.Option value={null}>全部</Select.Option>
+            {STATUS.map((item) => (
+              <Select.Option key={item.value}>{item.label}</Select.Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+  useEffect(() => {
+    handleSearch();
+    if (!userList.length > 0) dispatch({ type: 'user/fetch' });
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
+      </div>
+      <Table
+        loading={loading}
+        style={{ marginTop: 20 }}
+        rowKey={(record) =>
+          `${record.user_name}-${record.project_code}-${record.type_name}`
+        }
+        columns={columns}
+        dataSource={finance.list}
+        pagination={finance.pagination}
+        onChange={onChangePage}
+      />
+    </div>
+  );
+}
+
+export default connect(({ report, user, loading }) => ({
+  finance: report.finance,
+  userList: user.list,
+  loading: loading.models.report,
+}))(Finance);

+ 257 - 0
src/pages/PurchaseList/Report/Finance/Project.js

@@ -0,0 +1,257 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { connect } from 'dva';
+import {
+  Form,
+  Table,
+  DatePicker,
+  Input,
+  Button,
+  Select,
+  Modal,
+  Popover,
+} from 'antd';
+import { ProfileOutlined } from '@ant-design/icons';
+import styles from '../report.less';
+import { downloadFile, getToken } from '@/utils/utils.js';
+import { queryFinanceProjDetail } from '@/services/workHours';
+import dayjs from 'dayjs';
+
+const { RangePicker } = DatePicker;
+
+function FinanceProject(props) {
+  const { dispatch, loading, project, userList } = props;
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState(false);
+  const [current, setCurrent] = useState(null);
+
+  var initDate;
+  if (dayjs().date() > 25) {
+    initDate = [
+      dayjs().subtract(1, 'month').set('date', 26),
+      dayjs().set('date', 25),
+    ];
+  } else {
+    initDate = [
+      dayjs().subtract(2, 'month').set('date', 26),
+      dayjs().subtract(1, 'month').set('date', 25),
+    ];
+  }
+
+  const columns = [
+    // {
+    //   title: '序号',
+    //   render: (_, __, index) => {
+    //     const { current, pageSize } = project.pagination;
+    //     return (current - 1) * pageSize + index + 1;
+    //   },
+    // },
+    {
+      title: '项目名称',
+      dataIndex: 'name',
+      render: (name, item) => <a onClick={() => handleClick(item)}>{name}</a>,
+    },
+    { title: '项目编号', dataIndex: 'code' },
+    {
+      title: (
+        <div>
+          本月人日数
+          <Popover
+            content={
+              <div>
+                本月人日数为上一个工时周期(前月26日至当月25日)内的工时数,
+                <br />
+                与选取时间无关
+              </div>
+            }
+          >
+            <ProfileOutlined style={{ color: 'rgba(0,0,0,0.4)' }} />
+          </Popover>
+        </div>
+      ),
+      dataIndex: 'month_workload',
+    },
+    {
+      title: (
+        <div>
+          总人日数
+          <Popover content={<div>选取时间内的总工时数</div>}>
+            <ProfileOutlined style={{ color: 'rgba(0,0,0,0.4)' }} />
+          </Popover>
+        </div>
+      ),
+      dataIndex: 'total_workload',
+    },
+    { title: '项目预算人日 ', dataIndex: 'budget' },
+  ];
+
+  const filterRef = useRef({ pageSize: 20 });
+  const onChangePage = (pagination) => {
+    dispatch({
+      type: 'finance/queryFinanceProjReport',
+      payload: {
+        ...filterRef.current,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    const { time, user_id } = form.getFieldsValue();
+    filterRef.current.s_time = time[0]
+      ? dayjs(time[0]).format('YYYY-MM-DD')
+      : '';
+    filterRef.current.e_time = time[1]
+      ? dayjs(time[1]).format('YYYY-MM-DD')
+      : '';
+    filterRef.current.user_id = user_id || '';
+
+    dispatch({
+      type: 'finance/queryFinanceProjReport',
+      payload: {
+        ...filterRef.current,
+      },
+    });
+  };
+
+  const handleClick = (item) => {
+    setCurrent({
+      s_time: filterRef.current.s_time,
+      e_time: filterRef.current.e_time,
+      user_id: filterRef.current.user_id,
+      project_id: item.project_id,
+      project_code: item.code,
+      project_name: item.name,
+    });
+    setVisible(true);
+  };
+
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item label="填报人" name="user_id">
+          <Select
+            options={userList}
+            fieldNames={{ label: 'CName', value: 'ID' }}
+            style={{ width: 150 }}
+            showSearch
+            filterOption={(inputValue, option) =>
+              option.CName.includes(inputValue)
+            }
+            allowClear
+          />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  const handleDownload = () => {
+    const token = getToken();
+    const { user_id, time } = form.getFieldsValue();
+    let s_time = time[0] ? dayjs(time[0]).format('YYYY-MM-DD') : '';
+    let e_time = time[1] ? dayjs(time[1]).format('YYYY-MM-DD') : '';
+
+    downloadFile(
+      `/api/v2/workload/finance/exe/export?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}&user_id=${
+        user_id || ''
+      }`,
+      `财务报表_项目${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
+    );
+  };
+
+  useEffect(() => {
+    handleSearch();
+    if (!userList.length > 0) dispatch({ type: 'user/fetch' });
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
+      </div>
+      <Table
+        loading={loading}
+        style={{ marginTop: 20 }}
+        rowKey={`code`}
+        columns={columns}
+        dataSource={project.list}
+        pagination={project.pagination}
+        onChange={onChangePage}
+      />
+      <FinanceProjectModal
+        data={current}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      />
+    </div>
+  );
+}
+
+function FinanceProjectModal(props) {
+  const { visible, data, onCancel } = props;
+  const [list, setList] = useState([]);
+  const [loading, setLoading] = useState(false);
+  const columns = [
+    // { title: '项目名称', dataIndex: 'name' },
+    // { title: '项目编号', dataIndex: 'code' },
+    { title: '分项工作代码', dataIndex: 'name' },
+    { title: '本月人日数', dataIndex: 'month_workload' },
+    { title: '累计人日数', dataIndex: 'total_workload' },
+    { title: '项目预算人日 ', dataIndex: 'budget' },
+  ];
+
+  const queryList = async () => {
+    setLoading(true);
+    let res = await queryFinanceProjDetail({
+      s_time: data.s_time,
+      e_time: data.e_time,
+      user_id: data.user_id,
+      project_id: data.project_id,
+    });
+    if (res) {
+      setList(res.data);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    if (visible && data) {
+      queryList();
+    }
+    if (!visible) {
+      setList([]);
+    }
+  }, [visible]);
+
+  return (
+    <Modal
+      title={data ? `${data.project_name}【${data.project_code}】` : '详情'}
+      width="80%"
+      visible={visible}
+      onCancel={onCancel}
+      footer={false}
+      destroyOnClose
+    >
+      <Table
+        columns={columns}
+        loading={loading}
+        dataSource={list}
+        rowKey="name"
+      ></Table>
+    </Modal>
+  );
+}
+
+export default connect(({ finance, user, loading }) => ({
+  project: finance.project,
+  userList: user.list,
+  loading: loading.models.finance,
+}))(FinanceProject);

+ 112 - 0
src/pages/PurchaseList/Report/Finance/Resources.js

@@ -0,0 +1,112 @@
+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 dayjs from 'dayjs';
+import { downloadFile, getToken } from '@/utils/utils.js';
+
+const { RangePicker } = DatePicker;
+
+function FinanceResource(props) {
+  const { dispatch, loading, resource } = props;
+  const [form] = Form.useForm();
+  const columns = [
+    // {
+    //   title: '序号',
+    //   render: (_, __, index) => index + 1,
+    // },
+    { title: '事业部名称 ', dataIndex: 'dep_name' },
+    { title: '资源中心人日使用量', dataIndex: 'workload' },
+    { title: '技术中心人日数', dataIndex: 'tech_workload' },
+    { title: '招采中心人日数', dataIndex: 'purchase_workload' },
+    { title: '技术中心售前人日数', dataIndex: 'tech_sale_workload' },
+    { title: '技术中心执行人日数', dataIndex: 'tech_exe_workload' },
+    { title: '招采中心售前人日数', dataIndex: 'purchase_sale_workload' },
+    { title: '招采中心执行人日数', dataIndex: 'purchase_exe_workload' },
+  ];
+
+  var initDate;
+  if (dayjs().date() > 25) {
+    initDate = [
+      dayjs().subtract(1, 'month').set('date', 26),
+      dayjs().set('date', 25),
+    ];
+  } else {
+    initDate = [
+      dayjs().subtract(2, 'month').set('date', 26),
+      dayjs().subtract(1, 'month').set('date', 25),
+    ];
+  }
+
+  const handleSearch = () => {
+    const { time } = form.getFieldsValue();
+    let s_time = time[0] ? dayjs(time[0]).format('YYYY-MM-DD') : null;
+    let e_time = time[1] ? dayjs(time[1]).format('YYYY-MM-DD') : null;
+
+    dispatch({
+      type: 'finance/queryFinanceResReport',
+      payload: {
+        s_time,
+        e_time,
+      },
+    });
+  };
+
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  const handleDownload = () => {
+    const { time } = form.getFieldsValue();
+    const token = getToken();
+    let s_time = time[0] ? dayjs(time[0]).format('YYYY-MM-DD') : '';
+    let e_time = time[1] ? dayjs(time[1]).format('YYYY-MM-DD') : '';
+    console.log(
+      `/api/v2/workload/finance/resource/export?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`,
+    );
+
+    downloadFile(
+      `/api/v2/workload/finance/resource/export?JWT-TOKEN=${token}&s_time=${s_time}&e_time=${e_time}`,
+      `财务报表_资源${dayjs().format('YYYYMMDDHHMMSS')}.xlsx`,
+    );
+  };
+
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
+      </div>
+      <Table
+        loading={loading}
+        style={{ marginTop: 20 }}
+        rowKey={'dep_name'}
+        columns={columns}
+        dataSource={resource}
+        pagination={false}
+      />
+    </div>
+  );
+}
+
+export default connect(({ finance, loading }) => ({
+  resource: finance.resource,
+  loading: loading.models.finance,
+}))(FinanceResource);

+ 47 - 0
src/pages/PurchaseList/Report/Finance/models/finance.js

@@ -0,0 +1,47 @@
+import {
+  queryFinanceResReport,
+  queryFinanceProjReport,
+  queryFinanceProjDetail,
+} from '@/services/workHours';
+import { message } from 'antd';
+
+export default {
+  namespace: 'finance',
+  state: {
+    resource: [],
+    project: {
+      list: [],
+      pagination: {},
+    },
+  },
+
+  effects: {
+    *queryFinanceResReport({ payload = {} }, { call, put }) {
+      const res = yield call(queryFinanceResReport, payload);
+      if (res) {
+        yield put({
+          type: 'save',
+          payload: { resource: res.data },
+        });
+      }
+    },
+    *queryFinanceProjReport({ payload = {} }, { call, put }) {
+      const res = yield call(queryFinanceProjReport, payload);
+      if (res) {
+        yield put({
+          type: 'save',
+          payload: { project: res.data },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 280 - 0
src/pages/PurchaseList/Report/Project.js

@@ -0,0 +1,280 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { connect } from 'dva';
+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 UserProjectRptModal from './UserProjectRptModal';
+import { downloadFile, getToken } from '@/utils/utils.js';
+
+const { Option } = Select;
+const { RangePicker } = DatePicker;
+const initDate = [moment().startOf('years'), moment()];
+
+// var currentYear = new Date().getFullYear();
+// var yearList = [];
+// // 获得过去10年至未来20年的年份列表
+// for (var i = currentYear - 10; i < currentYear + 20; i++) {
+//   yearList.push(i);
+// }
+
+function Resource(props) {
+  const { dispatch, loading, project } = props;
+  const [form] = Form.useForm();
+  const [expandedRowKeys, setExpandedRowKeys] = useState([]);
+  const [visible, setVisible] = useState(false);
+  const [modalFilter, setModalFilter] = useState({});
+  const filterRef = useRef({});
+  // const onChangePage = pagination => {
+  //   dispatch({
+  //     type: 'report/queryProjectReport',
+  //     payload: {
+  //       ...filterRef.current,
+  //       currentPage: pagination.current,
+  //     },
+  //   });
+  // };
+  const getMonthColumns = () => {
+    let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+    var time;
+    if (filterRef.current.s_time) {
+      time = [moment(filterRef.current.s_time), moment(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 cYear, cMonth;
+
+    // do {
+    //   cYear = current.year();
+    //   cMonth = current.month();
+    //   if (!date[cYear]) date[cYear] = [];
+    //   date[cYear].push(arr[cMonth]);
+    //   current.add('month', 1);
+    // } while (cYear != eYear || cMonth != eMonth);
+    for (let i = 0; i < 12; i++) {
+      cYear = current.year();
+      cMonth = current.month();
+      if (!date[cYear]) date[cYear] = [];
+      date[cYear].push(arr[cMonth]);
+      current.add('month', 1);
+    }
+    current.subtract('month', 12);
+    let monthColumns = Object.keys(date).map(year => ({
+      title: year + '年',
+      children: date[year].map(item => {
+        let key = current.format('YYYY-MM');
+        current.add('month', 1);
+        return {
+          title: `${item}`,
+          render: record => {
+            const { month } = record;
+            return month.find(item => item.st == key)?.pass_audit_cnt || 0;
+            // return month[key]?.total_audit_cnt || 0;
+            // return (
+            //   <Popover
+            //     content={
+            //       <>
+            //         {`审核通过: ${month[key]?.pass_audit_cnt || 0}`}
+            //         <br />
+            //         {`审核中: ${month[key]?.pending_audit_cnt || 0}`}
+            //         <br />
+            //         {`审核拒绝: ${month[key]?.refuse_audit_cnt || 0}`}
+            //         <br />
+            //         {`未提审: ${month[key]?.refuse_audit_cnt || 0}`}
+            //       </>
+            //     }
+            //   >
+            //     {month[key]?.total_audit_cnt || 0}
+            //   </Popover>
+            // );
+            // return `${JSON.stringify(month)}`;
+          },
+        };
+      }),
+    }));
+    return monthColumns;
+  };
+  // const onExpand = (expanded, record) => {
+  //   if (expanded && !record.isLoad) {
+  //     dispatch({
+  //       type: 'report/queryProjectReportDetail',
+  //       payload: {
+  //         ...filterRef.current,
+  //         record: record,
+  //       },
+  //     });
+  //   }
+  // };
+
+  const handleSearch = () => {
+    const { time, project_name } = form.getFieldsValue();
+    if (
+      !moment(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;
+    dispatch({
+      type: 'report/queryProjectReport',
+      payload: {
+        ...filterRef.current,
+        project_name: project_name,
+      },
+    });
+  };
+  const handleDownload = () => {
+    const s_time = !filterRef.current.s_time ? '' : filterRef.current.s_time;
+    const e_time = !filterRef.current.e_time ? '' : filterRef.current.e_time;
+    const project_name = form.getFieldValue('project_name');
+    const token = getToken();
+    downloadFile(
+      `/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`
+    );
+  };
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item label="项目" name="project_name">
+          <Input />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+  const columns = [
+    {
+      title: '项目名称',
+      render: record =>
+        !record.child && record.total_audit_cnt ? (
+          <a onClick={() => showUserModal(record)}>{record.name}</a>
+        ) : (
+          record.name
+        ),
+    },
+    ...getMonthColumns(),
+    { title: '总计', dataIndex: 'total_audit_cnt' },
+  ];
+  // const columns = [
+  //   {
+  //     title: '项目名称',
+  //     render: record =>
+  //       !record.isParent ? (
+  //         <a onClick={() => showUserModal(record)}>{record.type_name}</a>
+  //       ) : (
+  //         record.type_name
+  //       ),
+  //   },
+  //   {
+  //     title: '分项工作代码',
+  //     align: 'center',
+  //     dataIndex: 'type_code',
+  //   },
+  //   // {
+  //   //   title: '项目名称',
+  //   //   render: record => (record.project_id == 0 ? '' : record.project_name),
+  //   // },
+  //   // {
+  //   //   title: '项目编号',
+  //   //   dataIndex: 'project_code',
+  //   // },
+  //   // {
+  //   //   title: '工作内容',
+  //   //   dataIndex: 'type_name',
+  //   // },
+  //   ...getMonthColumns(),
+  //   {
+  //     title: '总计',
+  //     dataIndex: 'month_rpt',
+  //     render: arr => (
+  //       <Popover
+  //         content={
+  //           <>
+  //             {`审核通过: ${
+  //               arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`审核中: ${
+  //               arr ? arr.reduce((total, item) => total + item.pending_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`审核拒绝: ${
+  //               arr ? arr.reduce((total, item) => total + item.refuse_audit_cnt, 0) : ''
+  //             }`}
+  //             <br />
+  //             {`未提审: ${arr ? arr.reduce((total, item) => total + item.un_audit_cnt, 0) : ''}`}
+  //           </>
+  //         }
+  //       >
+  //         {arr ? arr.reduce((total, item) => total + item.total_audit_cnt, 0) : ''}
+  //       </Popover>
+  //     ),
+  //   },
+  // ];
+  const showUserModal = item => {
+    const { s_time, e_time } = filterRef.current;
+    setModalFilter({
+      s_time: s_time,
+      e_time: e_time,
+      type_id: item.id,
+      project_id: item.project_id,
+      flag: item.flag,
+    });
+    setVisible(true);
+  };
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  // useEffect(() => {
+  //   setExpandedRowKeys(project.list.map(item => item.id));
+  // }, [project.list]);
+
+  return (
+    <div className={styles.page}>
+      <div className={styles.topPart}>
+        {renderSearch()}
+        <Button type="primary" onClick={handleDownload}>
+          导出
+        </Button>
+      </div>
+      <Table
+        style={{ marginTop: 20 }}
+        columns={columns}
+        rowKey="key"
+        childrenColumnName="child"
+        // onExpand={onExpand}
+        dataSource={project}
+        pagination={false}
+      />
+      <UserProjectRptModal
+        filter={modalFilter}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      />
+    </div>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  project: report.project,
+  // projectList: report.projectList,
+  loading: loading.models.report,
+}))(Resource);

+ 240 - 0
src/pages/PurchaseList/Report/ProjectTree.js

@@ -0,0 +1,240 @@
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import { connect } from 'dva';
+import { Form, Table, DatePicker, Input, Button, Select, Modal } from 'antd';
+import styles from './report.less';
+import { downloadFile, getToken } from '@/utils/utils.js';
+import { queryFinanceProjDetail } from '@/services/workHours';
+import dayjs from 'dayjs';
+
+const { RangePicker } = DatePicker;
+
+function ProjectTree(props) {
+  const { dispatch, loading, projectNew } = props;
+  const [form] = Form.useForm();
+  const [visible, setVisible] = useState(false);
+  const [current, setCurrent] = useState(null);
+  const [columnsFilter, setColumnsFilter] = useState({
+    budget: false,
+    dep: false,
+  });
+
+  var initDate;
+  if (dayjs().date() > 25) {
+    initDate = [
+      dayjs().subtract(1, 'month').set('date', 26),
+      dayjs().set('date', 25),
+    ];
+  } else {
+    initDate = [
+      dayjs().subtract(2, 'month').set('date', 26),
+      dayjs().subtract(1, 'month').set('date', 25),
+    ];
+  }
+
+  const columns = useMemo(() => {
+    let arr = [
+      {
+        title: '名称',
+        dataIndex: 'name',
+        // render: (name, item) => <a onClick={() => handleClick(item)}>{name}</a>,
+      },
+      {
+        title: '本月待审核工时',
+        dataIndex: 'month_pending_audit_cnt',
+        width: '15%',
+      },
+      {
+        title: '本月已拒绝工时',
+        dataIndex: 'month_refuse_audit_cnt',
+        width: '15%',
+      },
+      {
+        title: '本月已审核工时',
+        dataIndex: 'month_pass_audit_cnt',
+        width: '15%',
+      },
+      { title: '累计有效工时', dataIndex: 'total_workload', width: '15%' },
+    ];
+    if (columnsFilter.budget) {
+      arr.push({
+        title: '预算 ',
+        dataIndex: 'budget',
+        width: '10%',
+        render: (_, record) =>
+          record.cond?.project_id && record.cond?.status == 0
+            ? record.budget === null
+              ? '未设置'
+              : record.budget
+            : null,
+      });
+    }
+    // if (columnsFilter.dep) {
+    //   arr.splice(1, 0, { title: '所属部门', dataIndex: 'dep_name', width: '15%' });
+    // }
+    return arr;
+  }, [columnsFilter]);
+
+  const filterRef = useRef({});
+  const cacheRef = useRef({});
+  const onChangePage = (pagination) => {
+    dispatch({
+      type: 'report/queryProjectReportNew',
+      payload: {
+        ...filterRef.current,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    const { time } = form.getFieldsValue();
+    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/queryProjectReportNew',
+      payload: {
+        ...filterRef.current,
+      },
+    });
+  };
+
+  const handleClick = (item) => {
+    setCurrent({
+      s_time: filterRef.current.s_time,
+      e_time: filterRef.current.e_time,
+      project_id: item.project_id,
+    });
+    setVisible(true);
+  };
+
+  const onExpand = (expanded, record) => {
+    // 加入缓存
+    cacheRef.current[record.key] = record;
+  };
+  const onExpandedRowsChange = (expandedRows) => {
+    let budget = false,
+      dep = false;
+    // 根据表格当前展开项决定是否显示预算和部门
+    const fn = (data) => {
+      data.forEach((record) => {
+        // 判断是否展开
+        if (expandedRows.includes(record.key)) {
+          // 执行项目被展开则显示预算
+          if (record.cond.status == 0) {
+            budget = true;
+          }
+          // 子集含有部门则显示部门列
+          if (record.child[0]?.dep_name) {
+            dep = true;
+          }
+          if (record.child) fn(record.child);
+        }
+      });
+    };
+    fn(projectNew);
+
+    setColumnsFilter({ budget, dep });
+  };
+
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={initDate}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  return (
+    <div>
+      <div className={styles.topPart}>{renderSearch()}</div>
+      <Table
+        loading={loading}
+        style={{ marginTop: 20 }}
+        rowKey={`key`}
+        columns={columns}
+        dataSource={projectNew}
+        expandable={{
+          childrenColumnName: 'child',
+          onExpand,
+          onExpandedRowsChange,
+        }}
+        // pagination={projectNew.pagination}
+        // onChange={onChangePage}
+        pagination={false}
+      />
+      <ProjectTreeModal
+        data={current}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      />
+    </div>
+  );
+}
+
+function ProjectTreeModal(props) {
+  const { visible, data, onCancel } = props;
+  const [list, setList] = useState([]);
+  const [loading, setLoading] = useState(false);
+  const columns = [
+    { title: '项目名称', dataIndex: 'name' },
+    { title: '项目编号', dataIndex: 'name' },
+    { title: '分项工作代码', dataIndex: 'name' },
+    { title: '本月人日数', dataIndex: 'name' },
+    { title: '累计人日数', dataIndex: 'name' },
+    { title: '项目预算人日 ', dataIndex: 'name' },
+  ];
+
+  const queryList = async () => {
+    setLoading(true);
+    let res = await queryFinanceProjDetail(data);
+    if (res) {
+      setList(res.data);
+    }
+    setLoading(false);
+  };
+  useEffect(() => {
+    if (visible && data) {
+      queryList();
+    }
+    if (!visible) {
+      setList([]);
+    }
+  }, [visible, data]);
+
+  return (
+    <Modal
+      title="项目详情"
+      width="80%"
+      open={visible}
+      onCancel={onCancel}
+      footer={false}
+      destroyOnClose
+    >
+      <Table
+        columns={columns}
+        loading={loading}
+        dataSource={list}
+        rowKey="project_name"
+      ></Table>
+    </Modal>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  projectNew: report.projectNew,
+  loading: loading.models.report,
+}))(ProjectTree);

+ 139 - 0
src/pages/PurchaseList/Report/Resource.js

@@ -0,0 +1,139 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { connect } from 'dva';
+import { Form, Table, DatePicker, Input, Button } from 'antd';
+import report from './models/report';
+import dayjs from 'dayjs';
+
+const { RangePicker } = DatePicker;
+
+function Resource(props) {
+  const { dispatch, form, loading, res } = props;
+  const columns = [
+    {
+      title: '用户名称',
+      dataIndex: 'user_name',
+    },
+    // {
+    //   title: '资源编号',
+    //   dataIndex: 'name',
+    // },
+    {
+      title: '部门',
+      dataIndex: 'dep_name',
+    },
+    {
+      title: '报工类型',
+      dataIndex: 'type_type_id',
+      render: (type) => (type == 1 ? '非项目' : '项目'),
+    },
+    {
+      title: '工时类型',
+      dataIndex: 'type_name',
+    },
+    {
+      title: '项目名称',
+      dataIndex: 'project_name',
+    },
+    {
+      title: '项目编号',
+      dataIndex: 'project_code',
+    },
+    {
+      title: '项目归属部门',
+      dataIndex: 'project_dep_name',
+    },
+    {
+      title: '工时统计',
+      children: [
+        {
+          title: '保存未提交',
+          dataIndex: 'un_audit_cnt',
+        },
+        {
+          title: '提交未审批',
+          dataIndex: 'pending_audit_cnt',
+        },
+        {
+          title: '审批通过',
+          dataIndex: 'pass_audit_cnt',
+        },
+        {
+          title: '已拒绝',
+          dataIndex: 'refuse_audit_cnt',
+        },
+      ],
+    },
+  ];
+  const filterRef = useRef({});
+  const onChangePage = (pagination) => {
+    dispatch({
+      type: 'report/queryResReport',
+      payload: {
+        ...filterRef.current,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    form.validateFields((error, { time }) => {
+      let params = {};
+      params.s_time = time[0]
+        ? dayjs(time[0]).format('YYYY-MM-DD 00:00:00')
+        : null;
+      params.e_time = time[1]
+        ? dayjs(time[1]).format('YYYY-MM-DD 23:59:59')
+        : null;
+      filterRef.current = params;
+
+      dispatch({
+        type: 'report/queryResReport',
+        payload: params,
+      });
+    });
+  };
+  const renderSearch = () => {
+    const formItemLayout = {
+      labelCol: { span: 5 },
+      wrapperCol: { span: 18 },
+    };
+
+    return (
+      <Form layout="inline" {...formItemLayout}>
+        <Form.Item label="时间">
+          {form.getFieldDecorator('time')(
+            <RangePicker placeholder="选择时间" />,
+          )}
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" loading={loading} onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+    );
+  };
+
+  useEffect(() => {
+    dispatch({
+      type: 'report/queryResReport',
+    });
+  }, []);
+
+  return (
+    <div>
+      {renderSearch()}
+      <Table
+        style={{ marginTop: 20 }}
+        columns={columns}
+        dataSource={res.list}
+        pagination={res.pagination}
+        onChange={onChangePage}
+      />
+    </div>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  res: report.res,
+  loading: loading.models.report,
+}))(Form.create()(Resource));

+ 126 - 0
src/pages/PurchaseList/Report/UserProjectRptModal.js

@@ -0,0 +1,126 @@
+import React, { useState, useEffect } from 'react';
+import { Modal, Table, Form, Input, Button } from 'antd';
+import { connect } from 'dva';
+import moment from 'moment';
+
+function UserProjectRptModal(props) {
+  const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
+  const [form] = Form.useForm();
+  // const getMonthColumns = () => {
+  //   let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+
+  //   return {
+  //     title: '月份',
+  //     children: arr.map((item, index) => ({
+  //       title: `${item}`,
+  //       dataIndex: `month[${String(index)}].total_audit_cnt`,
+  //       render: cnt => cnt || 0,
+  //     })),
+  //   };
+  // };
+  const getMonthColumns = () => {
+    let arr = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
+    var time = [moment(filter.s_time), moment(filter.e_time)];
+    var date = {};
+    let eYear = time[1].year();
+    let eMonth = time[1].month();
+    let current = moment(time[0]);
+    let cYear, cMonth;
+
+    for (let i = 0; i < 12; i++) {
+      cYear = current.year();
+      cMonth = current.month();
+      if (!date[cYear]) date[cYear] = [];
+      date[cYear].push(arr[cMonth]);
+      current.add('month', 1);
+    }
+    current.subtract('month', 12);
+    let monthColumns = Object.keys(date).map(year => ({
+      title: year + '年',
+      children: date[year].map(item => {
+        let key = current.format('YYYY-MM');
+        current.add('month', 1);
+        return {
+          title: `${item}`,
+          render: record => {
+            const { month_rpt } = record;
+            return month_rpt.find(item => item.ts == key)?.pass_audit_cnt || 0;
+          },
+        };
+      }),
+    }));
+    return monthColumns;
+  };
+  const columns = [
+    {
+      title: '用户名称',
+      dataIndex: 'c_name',
+    },
+    {
+      title: '部门',
+      dataIndex: 'dep_name',
+    },
+    // getMonthColumns(),
+    ...getMonthColumns(),
+    {
+      title: '总计',
+      dataIndex: 'month_rpt',
+      render: arr => (arr ? arr.reduce((total, item) => total + item.pass_audit_cnt, 0) : ''),
+    },
+  ];
+  const onChangePage = pagination => {
+    dispatch({
+      type: 'report/queryUserProjectReport',
+      payload: {
+        ...filter,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  const handleSearch = () => {
+    // if (!filter?.project_id && !filter?.p_type_id) return;
+    if (!filter?.classification) return;
+    const dep_name = form.getFieldValue('dep_name');
+    dispatch({
+      type: 'report/queryUserProjectReport',
+      payload: {
+        ...filter,
+        dep_name: dep_name,
+      },
+    });
+  };
+  useEffect(() => {
+    if (!filter?.project_id) return;
+    dispatch({
+      type: 'report/queryUserProjectReport',
+      payload: filter,
+    });
+  }, [filter]);
+
+  return (
+    <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="请输入部门" />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" onClick={handleSearch}>
+            查询
+          </Button>
+        </Form.Item>
+      </Form>
+      <Table
+        columns={columns}
+        loading={loading}
+        dataSource={data.list}
+        pagination={data.pagination}
+        onChange={onChangePage}
+      ></Table>
+    </Modal>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  data: report.projectUser,
+  loading: loading.models.report,
+}))(UserProjectRptModal);

+ 97 - 0
src/pages/PurchaseList/Report/UserRptModal.js

@@ -0,0 +1,97 @@
+import React, { useState, useEffect } from 'react';
+import { Modal, Table } from 'antd';
+import { connect } from 'dva';
+import moment from 'moment';
+
+function UserRptModal(props) {
+  const { dispatch, visible, onOk, onCancel, data, filter, loading } = props;
+  const columns = [
+    {
+      title: '用户名称',
+      dataIndex: 'c_name',
+    },
+    {
+      title: '员工号',
+      dataIndex: 'user_name',
+    },
+    {
+      title: '执行项目人日',
+      dataIndex: 'type_project_cnt',
+    },
+    {
+      title: '售前支持',
+      dataIndex: 'type_sale_cnt',
+    },
+    {
+      title: '市场品牌',
+      dataIndex: 'type_market_cnt',
+    },
+    {
+      title: '日常',
+      dataIndex: 'type_normal_cnt',
+    },
+    {
+      title: '标准化',
+      dataIndex: 'type_standardize_cnt',
+    },
+    {
+      title: '研发',
+      dataIndex: 'type_rd_cnt',
+    },
+    {
+      title: '漏填工时',
+      dataIndex: 'type_lost_cnt',
+    },
+    {
+      title: '总计',
+      dataIndex: 'total_cnt',
+    },
+    {
+      title: '有效利用率',
+      dataIndex: 'usage_percent',
+      render: (percent = 0) => (percent * 100).toFixed(2) + '%',
+    },
+    // {
+    //   title: '付费工时数',
+    //   dataIndex: 'pay_workload_cnt',
+    // },
+    // {
+    //   title: '付费工时率',
+    //   dataIndex: 'pay_workload_percent',
+    //   render: (percent = 0) => (percent * 100).toFixed(2) + '%',
+    // },
+  ];
+  const onChangePage = pagination => {
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: {
+        ...filter,
+        currentPage: pagination.current,
+      },
+    });
+  };
+  useEffect(() => {
+    if (!filter?.dep_id) return;
+    dispatch({
+      type: 'report/queryUserReport',
+      payload: filter,
+    });
+  }, [filter]);
+
+  return (
+    <Modal title="工时" width="80%" visible={visible} onCancel={onCancel} footer={false}>
+      <Table
+        columns={columns}
+        loading={loading}
+        dataSource={data.list}
+        pagination={data.pagination}
+        onChange={onChangePage}
+      ></Table>
+    </Modal>
+  );
+}
+
+export default connect(({ report, loading }) => ({
+  data: report.depUser,
+  loading: loading.models.report,
+}))(UserRptModal);

+ 326 - 0
src/pages/PurchaseList/Report/models/report.js

@@ -0,0 +1,326 @@
+import {
+  queryResReport,
+  queryUserReport,
+  queryDepReport,
+  queryProjectReport,
+  queryProjectReportDetail,
+  queryUserProjectReport,
+  queryProject,
+  queryDepCompare,
+  queryDepCompareUser,
+  queryFinanceReport,
+  queryUserProject,
+  queryProjectReportNew,
+} from '@/services/workHours';
+import { message } from 'antd';
+
+export default {
+  namespace: 'report',
+  state: {
+    res: {
+      list: [],
+      pagination: {},
+    },
+    depUser: {
+      list: [],
+      pagination: {},
+    },
+    projectUser: {
+      list: [],
+      pagination: {},
+    },
+    dep: {
+      list: [],
+      pagination: {},
+    },
+    // project: {
+    //   list: [],
+    //   pagination: {},
+    // },
+    project: [],
+    finance: {
+      list: [],
+      pagination: {},
+    },
+    depCompare: [],
+    depUserProject: [],
+    projectList: [],
+    projectNew: [],
+  },
+
+  effects: {
+    *queryProject(_, { call, put }) {
+      const { data } = yield call(queryProject, {});
+      yield put({
+        type: 'save',
+        payload: { projectList: data.list },
+      });
+    },
+    *queryResReport({ payload }, { call, put }) {
+      const { data } = yield call(queryResReport, payload);
+      yield put({
+        type: 'save',
+        payload: { res: data },
+      });
+    },
+    *queryUserReport({ payload }, { call, put }) {
+      const { data } = yield call(queryUserReport, payload);
+      yield put({
+        type: 'save',
+        payload: { depUser: data },
+      });
+    },
+    *queryUserProjectReport({ payload }, { call, put }) {
+      const { data } = yield call(queryUserProjectReport, payload);
+      // try {
+      //   data.list.forEach(item => {
+      //     let month = {};
+      //     item.month_rpt.forEach(mItem => {
+      //       let m = moment(mItem.ts).month();
+      //       month[m] = mItem;
+      //     });
+      //     item.month = month;
+      //   });
+      // } catch (error) {
+      //   console.error(error);
+      // }
+      yield put({
+        type: 'save',
+        payload: { projectUser: data },
+      });
+    },
+    *queryDepReport({ payload = {}, callback }, { call, put, select }) {
+      const { filter = {}, record = {} } = payload;
+      const dep = yield select((s) => s.report.dep);
+      const { data } = yield call(queryDepReport, {
+        ...filter,
+        dep_id: record.dep_id,
+      });
+      data.list.forEach((item) => {
+        if (item.sub_dep_num) {
+          item.children = [];
+          item.isLeaf = false;
+        } else {
+          item.isLeaf = true;
+        }
+        item.isLoad = false;
+      });
+
+      const find = (data, id, callback) => {
+        data.forEach((item) => {
+          if (item.dep_id == id) {
+            callback(item);
+          }
+          if (item.children) find(item.children, id, callback);
+        });
+      };
+
+      // 判断是否为根节点
+      if (record.dep_id) {
+        let currentItem;
+        find(dep.list, record.dep_id, (item) => {
+          item.children = data.list.length == 0 ? null : data.list;
+          item.isLoad = true;
+        });
+        yield put({
+          type: 'save',
+          payload: { dep: { ...dep, list: [...dep.list] } },
+        });
+      } else {
+        yield put({
+          type: 'save',
+          payload: { dep: data },
+        });
+      }
+      callback?.(data.list);
+    },
+    *queryProjectReport({ payload }, { call, put }) {
+      const { data } = yield call(queryProjectReport, {
+        ...payload,
+      });
+      const idHelper = (item, params) => {
+        if (params.flag === undefined) {
+          item.child.forEach((childItem) =>
+            idHelper(childItem, { flag: item.id }),
+          );
+          item.key = 'f' + item.id;
+        } else {
+          item.flag = params.flag;
+          if (params.project_id === undefined) {
+            item.child?.forEach((childItem) =>
+              idHelper(childItem, { flag: item.flag, project_id: item.id }),
+            );
+            item.key = 'f' + item.flag + 'p' + item.id;
+          } else {
+            item.project_id = params.project_id;
+            item.key = 'f' + item.flag + 'p' + item.project_id + 't' + item.id;
+          }
+        }
+      };
+      data.forEach((item) => idHelper(item, {}));
+      yield put({
+        type: 'save',
+        payload: {
+          project: data,
+        },
+      });
+    },
+    // *queryProjectReport({ payload }, { call, put }) {
+    //   const { data } = yield call(queryProjectReport, {
+    //     ...payload,
+    //     // pageSize: 20,
+    //   });
+    //   let treeData = {};
+    //   try {
+    //     data.list.forEach(item => {
+    //       let p_type_id =
+    //         item.rpt.project_id == 0 ? 't' + item.rpt.p_type_id : 'p' + item.rpt.project_id;
+    //       treeData[p_type_id] = {
+    //         id: p_type_id,
+    //         type_name:
+    //           item.rpt.project_id == 0
+    //             ? item.rpt.project_name
+    //             : `${item.rpt.project_name}(${item.rpt.project_code})`,
+    //         project_name: item.rpt.project_name,
+    //         project_id: item.rpt.project_id,
+    //         project_code: item.rpt.project_code,
+    //         p_type_id: item.rpt.p_type_id,
+    //         month_rpt: item.rpt.month_rpt,
+    //         classification: item.classification,
+    //         project_ids: item.project_ids,
+    //         type_code: '',
+    //         children: [],
+    //         month: {},
+    //         isLoad: false,
+    //         isParent: true,
+    //       };
+    //       item.rpt.month_rpt.forEach(mItem => {
+    //         // let m = moment(mItem.ts).month();
+    //         treeData[p_type_id].month[mItem.ts] = mItem;
+    //       });
+    //     });
+    //   } catch (error) {
+    //     console.log(error);
+    //   }
+    //   console.log(treeData);
+    //   yield put({
+    //     type: 'save',
+    //     payload: {
+    //       project: {
+    //         list: Object.values(treeData),
+    //         pagination: data.pagination,
+    //       },
+    //     },
+    //   });
+    // },
+    *queryProjectReportDetail({ payload = {} }, { call, put, select }) {
+      const { record = {} } = payload;
+      const { data } = yield call(queryProjectReportDetail, {
+        s_time: payload.s_time,
+        e_time: payload.e_time,
+        classification: record.classification,
+        p_type_id: record.p_type_id,
+        project_ids: record.project_ids.join(','),
+      });
+      data.forEach((item, index) => {
+        let month = {};
+        item.month_rpt.forEach((mItem) => {
+          // let m = moment(mItem.ts).month();
+          month[mItem.ts] = mItem;
+        });
+
+        item.month = month;
+        item.id = record.id + '_' + index;
+      });
+      const project = yield select((s) => s.report.project);
+      record.children =
+        data.length == 0
+          ? null
+          : data.map((item) => ({ ...item, project_ids: record.project_ids }));
+      record.isLoad = true;
+      yield put({
+        type: 'save',
+        payload: { project: { ...project } },
+      });
+    },
+    *queryFinanceReport({ payload = {} }, { call, put }) {
+      const { data } = yield call(queryFinanceReport, payload);
+      yield put({
+        type: 'save',
+        payload: { finance: data },
+      });
+    },
+    *queryDepCompare({ payload }, { call, put }) {
+      let { data } = yield call(queryDepCompare, payload);
+      if (data) {
+        const treeHelper = (item) => {
+          item.key = 'd' + item.dep_id;
+          if (!item.child && item.total_cnt) item.child = [];
+          item.child &&
+            item.child.forEach((childItem) => treeHelper(childItem));
+        };
+        data.forEach((item) => treeHelper(item));
+      } else data = [];
+      yield put({
+        type: 'save',
+        payload: { depCompare: data },
+      });
+    },
+    *queryDepCompareUser({ payload }, { call, put, select }) {
+      try {
+        let record = payload.record;
+        payload.dep_id = record.dep_id;
+        delete payload.record;
+        let { data } = yield call(queryDepCompareUser, payload);
+        data.forEach((item) => {
+          item.key = 'u' + item.user_id;
+          item.dep_id = payload.dep_id;
+        });
+        record.child = [...record.child, ...data];
+        record.isLoad = true;
+      } catch (error) {
+        console.log(error);
+      }
+      const depCompare = yield select((s) => s.report.depCompare);
+      yield put({
+        type: 'save',
+        payload: { depCompare: [...depCompare] },
+      });
+    },
+    *queryDepUserProject({ payload }, { call, put }) {
+      const { data } = yield call(queryUserProject, payload);
+      yield put({
+        type: 'save',
+        payload: { depUserProject: data },
+      });
+    },
+
+    *queryProjectReportNew({ payload }, { call, put }) {
+      const res = yield call(queryProjectReportNew, payload);
+      if (!res) return;
+      const initData = (data, parentKey) => {
+        let currentKey = parentKey + (data.id || data.name);
+        data.key = currentKey;
+        if (data.child) {
+          data.child.forEach((item) => {
+            initData(item, currentKey + '-');
+          });
+        }
+      };
+      initData(res.data, '');
+      yield put({
+        type: 'save',
+        payload: { projectNew: res.data.child || [] },
+      });
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 13 - 0
src/pages/PurchaseList/Report/report.less

@@ -0,0 +1,13 @@
+.page {
+  :global {
+    .ant-table table .ant-table-thead>tr>th {
+      border-top: 1px solid #e8e8e8;
+    }
+  }
+}
+
+.topPart{
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}

+ 0 - 61
src/pages/PurchaseList/RightContent.js

@@ -1,61 +0,0 @@
-import React, { PureComponent } from 'react';
-import { FormattedMessage } from 'umi/locale';
-import { LogoutOutlined } from '@ant-design/icons';
-import { Spin, Tag, Menu, Avatar } from 'antd';
-import HeaderDropdown from '@/components/HeaderDropdown';
-import styles from '@/components/GlobalHeader/index.less';
-import { connect } from 'dva';
-import { Logout } from '@/services/PurchaseList';
-import router from 'umi/router';
-
-@connect(({ user }) => ({
-  currentUser: user.currentUser,
-}))
-class GlobalHeaderRight extends PureComponent {
-  async onMenuClick() {
-    // this.props.dispatch({
-    //   type: 'purchaseList/logout',
-    // });
-    await Logout();
-    router.push('/login');
-  }
-  render() {
-    const { currentUser = {} } = this.props;
-    const theme = 'dark';
-    const menu = (
-      <Menu className={styles.menu} selectedKeys={[]} onClick={() => this.onMenuClick()}>
-        <Menu.Item key="logout">
-          <LogoutOutlined />
-          <FormattedMessage id="menu.account.logout" defaultMessage="logout" />
-        </Menu.Item>
-      </Menu>
-    );
-
-    let className = styles.right;
-    if (theme === 'dark') {
-      className = `${styles.right}  ${styles.dark}`;
-    }
-    return (
-      <div className={className}>
-        {currentUser.CName ? (
-          <div className={styles.userbox}>
-            <HeaderDropdown overlay={menu}>
-              <span className={`${styles.action} ${styles.account}`}>
-                <Avatar
-                  size="small"
-                  className={styles.avatar}
-                  src="https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png"
-                  alt="avatar"
-                />
-                <span className={styles.name}>{currentUser.CName}</span>
-              </span>
-            </HeaderDropdown>
-          </div>
-        ) : (
-          <Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />
-        )}
-      </div>
-    );
-  }
-}
-export default GlobalHeaderRight;

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

@@ -17,15 +17,18 @@ import RejectModal from './RejectModal';
 import AuthWorkList from './AuthWorkList';
 import { connect } from 'dva';
 import styles from './index.less';
-import moment from 'moment';
+import dayjs from 'dayjs';
+import { useRequest, useModel } from '@umijs/max';
 
 function List(props) {
-  const { dispatch, loading, list, project, currentUser, allType, user } =
-    props;
+  const {
+    initialState: { user },
+  } = useModel('@@initialState');
+  const { dispatch, loading, list, project, userList, allType } = props;
   const [visible, setVisible] = useState(false);
   const [filter, setFilter] = useState({});
   const [current, setCurrent] = useState({
-    date: moment(),
+    date: dayjs(),
     list: [],
   });
 
@@ -96,9 +99,9 @@ function List(props) {
     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 = moment(s_date)
-        .add('month', 1)
-        .add('days', -1)
+      const e_date = dayjs(s_date)
+        .add(1, 'month')
+        .add(-1, 'days')
         .format('YYYY-MM-DD');
       dispatch({
         type: 'workload/queryAuthWorkHours',
@@ -132,15 +135,15 @@ function List(props) {
   };
 
   useEffect(() => {
-    if (!currentUser.ID) return;
+    if (!user.ID) return;
     // 查询分类以及工时
     dispatch({
       type: 'workload/queryWorkType',
       callback: () => {
         const s_date = current.date.format('YYYY-MM') + '-01';
-        const e_date = moment(s_date)
-          .add('month', 1)
-          .add('days', -1)
+        const e_date = dayjs(s_date)
+          .add(1, 'month')
+          .add(-1, 'days')
           .format('YYYY-MM-DD');
         dispatch({
           type: 'workload/queryAuthWorkHours',
@@ -177,7 +180,7 @@ function List(props) {
         },
       });
     };
-  }, [currentUser.ID]);
+  }, [user.ID]);
 
   // useEffect(() => {
   //   onChangeDate(current.date);
@@ -201,7 +204,7 @@ function List(props) {
               list={getList()}
               onAgree={onAgree}
               onSearch={setFilter}
-              user={user}
+              user={userList}
               onReject={(records) => {
                 setVisible(true);
                 rejectRef.current = records;
@@ -221,9 +224,8 @@ function List(props) {
 
 export default connect(({ workload, user, loading }) => ({
   list: workload.list,
-  user: user.list,
+  userList: user.list,
   allType: workload.allType,
   project: workload.project,
-  currentUser: user.currentUser,
   loading: loading.models.workload,
 }))(List);

+ 0 - 1
src/pages/PurchaseList/WorkingHours/models/workingHours.js

@@ -8,7 +8,6 @@ import {
   queryProject,
   deleteWorkHour,
 } from '@/services/workHours.js';
-import { queryRole } from '@/services/SysAdmin';
 import { queryDepV2 } from '@/services/approval';
 import { message } from 'antd';
 import moment from 'moment';

+ 0 - 112
src/pages/PurchaseList/WorkloadIndex.js

@@ -1,112 +0,0 @@
-import React, { useEffect } from 'react';
-import { Layout, Menu } from 'antd';
-import { connect } from 'dva';
-import RightContent from './RightContent';
-import Link from 'umi/link';
-
-const { Header, Content, Footer } = Layout;
-const { SubMenu } = Menu;
-
-// 布局
-function LayoutDetail(props) {
-  const { currentUser, permission } = props;
-  const isAdmin = currentUser.UserName == 'admin';
-  var logoStyle = {
-    color: 'white',
-    fontWeight: 600,
-    fontSize: 20,
-    verticalAlign: 'middle',
-    marginRight: 60,
-    width: 120,
-  };
-  useEffect(() => {
-    // 查询用户信息
-    props.dispatch({
-      type: 'user/fetchCurrent',
-    });
-    localStorage.type = 'workload'
-  }, []);
-  const checkReport = state => {
-    if (isAdmin) return true;
-    const manager = currentUser.is_leader || currentUser.is_opt_mgr || currentUser.is_wty_mgr;
-    switch (state) {
-      case 0:
-        return currentUser.is_accountant || manager || permission['func-01-point-works-report'];
-      case 1:
-        return manager || permission['func-01-point-works-report-p'];
-      case 2:
-        return permission['func-01-point-works-report-d'];
-      case 3:
-        return currentUser.is_accountant || permission['func-01-point-works-report-p-s'];
-      case 4:
-        return permission['func-01-point-bom-flow'];
-    }
-  };
-  return (
-    <Layout>
-      <Header>
-        <div style={{ display: 'flex', height: '100%', justifyContent: 'space-between' }}>
-          <div style={{ display: 'flex', width: '70%' }}>
-            <div style={logoStyle}>金科环境</div>
-            <Menu
-              theme="dark"
-              mode="horizontal"
-              defaultSelectedKeys={[props.location.pathname]}
-              style={{ lineHeight: '64px', width: '100%' }}
-            >
-              <SubMenu key="/home/work-hours" title="工时管理">
-                <Menu.Item key="/home/work-hours">
-                  <Link to="/home/work-hours">上报工时</Link>
-                </Menu.Item>
-                <Menu.Item key="/home/work-hours-auth">
-                  <Link to="/home/work-hours-auth">审批工时</Link>
-                </Menu.Item>
-              </SubMenu>
-
-              <SubMenu key="/home/approval" title="项目立项">
-                <Menu.Item key="/home/approval/list">
-                  <Link to="/home/approval/list">项目列表</Link>
-                </Menu.Item>
-                <Menu.Item key="/home/approval/auth">
-                  <Link to="/home/approval/auth">审核列表</Link>
-                </Menu.Item>
-              </SubMenu>
-
-              {checkReport(0) && (
-                <SubMenu key="/home/report" title="工时报表">
-                  {/* <Menu.Item key="/home/report/resource">
-                  <Link to="/home/report/resource">资源报表</Link>
-                </Menu.Item> */}
-                  {checkReport(1) && (
-                    <Menu.Item key="/home/report/project">
-                      <Link to="/home/report/project">项目报表</Link>
-                    </Menu.Item>
-                  )}
-                  {checkReport(2) && (
-                    <Menu.Item key="/home/report/department">
-                      <Link to="/home/report/department">部门报表</Link>
-                    </Menu.Item>
-                  )}
-                  {checkReport(3) && (
-                    <Menu.Item key="/home/report/finance">
-                      <Link to="/home/report/finance">财务报表</Link>
-                    </Menu.Item>
-                  )}
-                </SubMenu>
-              )}
-            </Menu>
-          </div>
-          <RightContent />
-        </div>
-      </Header>
-      <Content style={{ padding: '0 50px', minHeight: 'calc(100vh - 64px)' }}>
-        <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>{props.children}</div>
-      </Content>
-      {/* <Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer> */}
-    </Layout>
-  );
-}
-export default connect(({ user }) => ({
-  currentUser: user.currentUser,
-  permission: user.currentUser.Permission,
-}))(LayoutDetail);

+ 129 - 106
src/services/approval.js

@@ -1,4 +1,4 @@
-import {request }from 'umi';
+import { request } from 'umi';
 import { stringify } from 'qs';
 
 //获取部门结构
@@ -6,108 +6,131 @@ export async function queryDepV2(params) {
   return request(`/api/v2/dep?${stringify(params)}`);
 }
 
-// export async function queryType() {
-//   return request(`/api/v2/approval/type/dic`);
-// }
-
-// export async function queryFlow() {
-//   return request(`/api/v2/approval/flow`);
-// }
-
-// export async function queryIndustry() {
-//   return request(`/api/v2/approval/industry/dic`);
-// }
-
-// // 提交立项
-// export async function createApproval(data) {
-//   return request(`/api/v2/approval/record`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// // 更新立项
-// export async function updateApproval(data) {
-//   return request(`/api/v2/approval/record`, {
-//     method: 'PUT',
-//     body: data,
-//   });
-// }
-
-// // 删除立项
-// export async function deleteApproval(data) {
-//   return request(`/api/v2/approval/record/${data.id}`, {
-//     method: 'DELETE',
-//   });
-// }
-
-// // 审批
-// export async function authApproval(data) {
-//   return request(`/api/v2/approval/auth`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// // 审批列表查询:获取当前用户可审批的立项列表
-// export async function queryAuth(data) {
-//   return request(`/api/v2/approval/list/auth?${stringify(data)}`);
-// }
-
-// // 查询立项 id=&pageSize=&currentPage=&user_id=
-// export async function queryApproval(data) {
-//   return request(`/api/v2/approval/record?${stringify(data)}`);
-// }
-
-// // 提交审核
-// export async function submitAudit(data) {
-//   return request(`/api/v2/approval/audit/submit`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-
-// //添加项目成员
-// export async function addMember(data) {
-//   return request(`/api/v2/project_code/user`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// //获取项目成员
-// export async function queryMember(params) {
-//   return request(`/api/v2/project_code/user?${stringify(params)}`);
-// }
-
-// //转执行
-// export async function startExecution(data) {
-//   return request(`/api/v2/project_code/to_exe`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// //转运营
-// export async function startOperate(data) {
-//   return request(`/api/v2/project_code/to_opt`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// //转质保
-// export async function startQuality(data) {
-//   return request(`/api/v2/project_code/to_wty`, {
-//     method: 'POST',
-//     body: data,
-//   });
-// }
-
-// //移除成员
-// export async function deleteMember(data) {
-//   return request(`/api/v2/project_code/user/${data.project_code_id}/${data.user_id}`, {
-//     method: 'DELETE',
-//   });
-// }
+export async function queryType() {
+  return request(`/api/v2/approval/type/dic`);
+}
+
+export async function queryFlow() {
+  return request(`/api/v2/approval/flow`);
+}
+
+export async function queryIndustry() {
+  return request(`/api/v2/approval/industry/dic`);
+}
+
+// 提交立项
+export async function createApproval(data) {
+  return request(`/api/v2/approval/record`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+// 更新立项
+export async function updateApproval(data) {
+  return request(`/api/v2/approval/record`, {
+    method: 'PUT',
+    body: data,
+  });
+}
+
+// 删除立项
+export async function deleteApproval(data) {
+  return request(`/api/v2/approval/record/${data.id}`, {
+    method: 'DELETE',
+  });
+}
+
+// 审批
+export async function authApproval(data) {
+  return request(`/api/v2/approval/auth`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+// 审批列表查询:获取当前用户可审批的立项列表
+export async function queryAuth(data) {
+  return request(`/api/v2/approval/list/auth?${stringify(data)}`);
+}
+
+// 查询立项 id=&pageSize=&currentPage=&user_id=
+export async function queryApproval(data) {
+  return request(`/api/v2/approval/record?${stringify(data)}`);
+}
+
+// 提交审核
+export async function submitAudit(data) {
+  return request(`/api/v2/approval/audit/submit`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//添加项目成员
+export async function addMember(data) {
+  return request(`/api/v2/project_code/user`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//获取项目成员
+export async function queryMember(params) {
+  return request(`/api/v2/project_code/user?${stringify(params)}`);
+}
+
+//转执行
+export async function startExecution(data) {
+  return request(`/api/v2/project_code/to_exe`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//转运营
+export async function startOperate(data) {
+  return request(`/api/v2/project_code/to_opt`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//转质保
+export async function startQuality(data) {
+  return request(`/api/v2/project_code/to_wty`, {
+    method: 'POST',
+    body: data,
+  });
+}
+
+//移除成员
+export async function deleteMember(data) {
+  return request(
+    `/api/v2/project_code/user/${data.project_code_id}/${data.user_id}`,
+    {
+      method: 'DELETE',
+    },
+  );
+}
+
+//获取项目预算
+export async function queryBudget(params) {
+  return request(`/api/v2/workload/project/budget?${stringify(params)}`);
+}
+
+//设定项目预算
+export async function setBudget(params) {
+  return request(`/api/v2/workload/project/budget`, {
+    method: 'POST',
+    data: params,
+  });
+}
+
+//修改项目经理
+export async function modifyManager(params) {
+  return request(`/api/v2/workload/project/change_pm`, {
+    method: 'POST',
+    data: params,
+  });
+}

+ 3 - 3
src/services/workHours.js

@@ -31,7 +31,7 @@ export async function queryProject(params) {
 export async function authWorkload(data) {
   return request(`/api/v2/workload/auths`, {
     method: 'POST',
-    body: data,
+    data,
   });
 }
 
@@ -45,14 +45,14 @@ export async function authWorkload(data) {
 export async function addWorkHours(data) {
   return request(`/api/v2/workload/record`, {
     method: 'POST',
-    body: data,
+    data,
   });
 }
 
 export async function addAuthWorkHours(data) {
   return request(`/api/v2/workload/records`, {
     method: 'POST',
-    body: data,
+    data,
   });
 }
 

+ 20 - 0
yarn.lock

@@ -4496,6 +4496,14 @@ eastasianwidth@^0.2.0:
   resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
 
+echarts@^5.4.2:
+  version "5.4.2"
+  resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.2.tgz#9f38781c9c6ae323e896956178f6956952c77a48"
+  integrity sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==
+  dependencies:
+    tslib "2.3.0"
+    zrender "5.4.3"
+
 electron-to-chromium@^1.4.284:
   version "1.4.356"
   resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.356.tgz#b75a8a8c31d571f6024310cc980a08cd6c15a8c5"
@@ -9679,6 +9687,11 @@ trim-newlines@^3.0.0:
   resolved "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
   integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
+tslib@2.3.0:
+  version "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:
   version "1.14.1"
   resolved "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -10196,3 +10209,10 @@ yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+zrender@5.4.3:
+  version "5.4.3"
+  resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.3.tgz#41ffaf835f3a3210224abd9d6964b48ff01e79f5"
+  integrity sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==
+  dependencies:
+    tslib "2.3.0"