Renxy 3 лет назад
Родитель
Сommit
0877ad6ea3
42 измененных файлов с 5587 добавлено и 0 удалено
  1. 49 0
      src/components/AuditForm/ComponentLibrary.js
  2. 92 0
      src/components/AuditForm/FormContent.js
  3. 326 0
      src/components/AuditForm/ItemAttribute.js
  4. 80 0
      src/components/AuditForm/constant.js
  5. 71 0
      src/components/AuditForm/index.js
  6. 384 0
      src/components/ChartUtils/utils.js
  7. 36 0
      src/components/DDComponents/DDAttachment/index.js
  8. 49 0
      src/components/DDComponents/DDCode/index.js
  9. 13 0
      src/components/DDComponents/DDDateField/index.js
  10. 22 0
      src/components/DDComponents/DDDateRangeField/index.js
  11. 30 0
      src/components/DDComponents/DDMultiSelectField/index.js
  12. 107 0
      src/components/DDComponents/DDPhotoField/AliyunOssUploader.js
  13. 46 0
      src/components/DDComponents/DDPhotoField/index.js
  14. 28 0
      src/components/DDComponents/DDSelectField/index.js
  15. 75 0
      src/components/DDComponents/DepartmentField/index.js
  16. 29 0
      src/components/DDComponents/InnerContactField/index.js
  17. 19 0
      src/components/DDComponents/NumberField/index.js
  18. 19 0
      src/components/DDComponents/PhoneField/index.js
  19. 90 0
      src/components/DDComponents/TableField/index.js
  20. 16 0
      src/components/DDComponents/TableField/index.less
  21. 124 0
      src/components/DDComponents/index.js
  22. 944 0
      src/components/DataMeter/config.js
  23. 233 0
      src/components/Flow/components/judgeComponent/index.tsx
  24. 76 0
      src/components/Flow/components/judgeModal/index.tsx
  25. 132 0
      src/components/Flow/node/auditNode/index.tsx
  26. 283 0
      src/components/Flow/node/auditNode/mapServe.tsx
  27. 113 0
      src/components/Flow/node/judgeNode/index.tsx
  28. 268 0
      src/components/Flow/node/judgeNode/mapServe.tsx
  29. 624 0
      src/models/dataMeterNew.js
  30. 64 0
      src/pages/DDLogin/index.js
  31. 0 0
      src/pages/Mobile/DataMeter/Alarm.js
  32. 118 0
      src/pages/Mobile/DataMeter/Chart.js
  33. 61 0
      src/pages/Mobile/DataMeter/Chart.less
  34. 39 0
      src/pages/Mobile/DataMeter/Craft.js
  35. 0 0
      src/pages/Mobile/DataMeter/Report.js
  36. 16 0
      src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.less
  37. 100 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/ApprovalProcess.tsx
  38. 130 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/AuditDetailed.js
  39. 271 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/AuditFlow.js
  40. 112 0
      src/pages/PurchaseAdmin/PurchaseList/Detail/HistoryDrawer.js
  41. 176 0
      src/pages/PurchaseAdmin/PurchaseList/Report/DepCompareModal.js
  42. 122 0
      src/pages/PurchaseAdmin/PurchaseList/Report/Finance.js

+ 49 - 0
src/components/AuditForm/ComponentLibrary.js

@@ -0,0 +1,49 @@
+import { message, Col, Modal, Row } from 'antd';
+import React, { useState } from 'react';
+import { COMPONENT_LIST } from './constant';
+
+function ComponentLibrary(props) {
+  const { visible, onCancel, onOk } = props;
+  const [currnetItem, setCurrentItem] = useState(null);
+
+  const handleOk = () => {
+    if (!currnetItem) {
+      message.error('请选择控件');
+      return;
+    }
+    setCurrentItem(null);
+    onOk?.(currnetItem);
+  };
+  const handleCancel = () => {
+    setCurrentItem(null);
+    onCancel?.();
+  };
+  return (
+    <Modal visible={visible} onCancel={handleCancel} onOk={handleOk}>
+      <Row gutter={12} style={{ paddingTop: 20 }}>
+        {COMPONENT_LIST.map(item => (
+          <Col span={8}>
+            <div
+              onClick={() => setCurrentItem(item)}
+              style={{
+                display: 'flex',
+                justifyContent: 'flex-start',
+                alignItems: 'center',
+                border: item == currnetItem ? '1px solid #1890FF' : '1px solid #aaa',
+                width: '100%',
+                padding: '4px 12px',
+                cursor: 'pointer',
+                margin: '10px 0',
+              }}
+            >
+              {item.icon}
+              <span style={{ marginLeft: 8 }}>{item.props.label}</span>
+            </div>
+          </Col>
+        ))}
+      </Row>
+    </Modal>
+  );
+}
+
+export default ComponentLibrary;

+ 92 - 0
src/components/AuditForm/FormContent.js

@@ -0,0 +1,92 @@
+import { Form } from 'antd';
+import React, { useState } from 'react';
+import { ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
+
+function FormContent(props) {
+  const { list, onChange, onSelect } = props;
+  const [currentItem, setCurrentItem] = useState(null);
+  const handleDelete = index => {
+    let _list = [...list];
+    _list.splice(index, 1);
+    onChange(_list);
+  };
+  const handleUp = index => {
+    let _list = [...list];
+    // 跟上一位置换位置
+    const temp = _list[index - 1];
+    _list[index - 1] = _list[index];
+    _list[index] = temp;
+    onChange(_list);
+  };
+  const handleDown = index => {
+    let _list = [...list];
+    const temp = _list[index + 1];
+    _list[index + 1] = _list[index];
+    _list[index] = temp;
+    onChange(_list);
+  };
+  const handleSelect = index => {
+    setCurrentItem(index);
+    onSelect(index);
+  };
+  return (
+    <div style={{ width: 300 }}>
+      {list.map((item, index) => {
+        const btns = (
+          <>
+            {index != 0 && (
+              <ArrowUpOutlined
+                style={{ marginLeft: 5, cursor: 'pointer' }}
+                onClick={() => handleUp(index)}
+              />
+            )}
+            {index != list.length - 1 && (
+              <ArrowDownOutlined
+                style={{ marginLeft: 5, cursor: 'pointer' }}
+                onClick={() => handleDown(index)}
+              />
+            )}
+            <DeleteOutlined
+              style={{ marginLeft: 5, cursor: 'pointer' }}
+              onClick={() => handleDelete(index)}
+            />
+          </>
+        );
+        return (
+          <FormItem
+            key={item.id}
+            active={index == currentItem}
+            onClick={() => handleSelect(index)}
+            item={item}
+            btns={btns}
+          />
+        );
+      })}
+    </div>
+  );
+}
+
+function FormItem(props) {
+  const { item, btns, active, onClick } = props;
+  const { label, placeholder, required } = item.props;
+  return (
+    <div
+      style={{
+        marginBottom: 20,
+        padding: '4px 12px',
+        border: '1px solid #666',
+        borderLeft: active ? '10px solid #1890FF' : '1px solid #666',
+      }}
+      onClick={onClick}
+    >
+      <div style={{ fontSzie: 24, color: '#000', fontWeight: 'bold', position: 'relative' }}>
+        {required && <i style={{ color: 'red' }}>*</i>}
+        {label}
+        <div style={{ position: 'absolute', right: 0, top: 0, padding: '5px 10px' }}>{btns}</div>
+      </div>
+      <div style={{ color: '#999', fontSize: 16 }}>{placeholder}</div>
+    </div>
+  );
+}
+
+export default FormContent;

+ 326 - 0
src/components/AuditForm/ItemAttribute.js

@@ -0,0 +1,326 @@
+import { Form, Button, Switch, Input, Radio, Space, Row } from 'antd';
+import React, { useMemo, useState, useEffect } from 'react';
+import { DeleteOutlined } from '@ant-design/icons';
+function ItemAttribute(props) {
+  const { item, onChange } = props;
+
+  const renderForm = () => {
+    let FormContent;
+    const formProps = {
+      btns: <Form.Item>
+        <Button type="primary" htmlType="submit" style={{ marginRight: 20 }}>
+          保存
+        </Button>
+      </Form.Item>,
+      item,
+      onFinish: values => {
+        console.log(values);
+        onChange?.(values);
+      }
+    }
+    switch (item.componentName) {
+      case 'InnerContactField':
+        FormContent = <InnerContactField {...formProps} />;
+        break;
+      case 'DepartmentField':
+        FormContent = <DepartmentField {...formProps} />;
+        break;
+      case 'TextField':
+        FormContent = <TextField {...formProps} />;
+        break;
+      case 'TextareaField':
+        FormContent = <TextareaField {...formProps} />;
+        break;
+      case 'DDSelectField':
+        FormContent = <DDSelectField {...formProps} />;
+        break;
+      case 'DDMultiSelectField':
+        FormContent = <DDMultiSelectField {...formProps} />;
+        break;
+      case 'NumberField':
+        FormContent = <NumberField {...formProps} />;
+        break;
+    }
+
+    return FormContent;
+  };
+
+  if (!item) return null;
+
+  return (
+    <div
+      style={{
+        // position: 'absolute',
+        // top: 0,
+        // right: 0,
+        width: 300,
+        height: '100%',
+        overflowY: 'auto',
+      }}
+    >
+      {renderForm()}
+    </div>
+  );
+}
+
+export default ItemAttribute;
+
+function InnerContactField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const onSwitchChange = checked => {
+    form.setFieldValue("choice", checked ? 1 : 0);
+  }
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="choice">
+        <Radio.Group>
+          <Space direction="vertical">
+            <Radio value={'0'}>只能选择一人</Radio>
+            <Radio value={'1'}>可同时选择多人</Radio>
+          </Space>
+        </Radio.Group>
+      </Form.Item>
+      <Form.Item label="必填" name="required" valuePropName="checked">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DepartmentField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="choice">
+        <Radio.Group>
+          <Space direction="vertical">
+            <Radio value={'0'}>只能选择一人</Radio>
+            <Radio value={'1'}>可同时选择多人</Radio>
+          </Space>
+        </Radio.Group>
+      </Form.Item>
+      <Form.Item label="必填" name="required" valuePropName="checked">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function TextField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+
+  const onReset = () => {
+    form.resetFields();
+  };
+  return (
+    <Form
+      form={form}
+      labelCol={{ span: 8 }}
+      wrapperCol={{ span: 16 }}
+      onFinish={onFinish}
+      autoComplete="off"
+      initialValues={item.props}
+    >
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function TextareaField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DDSelectField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const handleFinish = value => {
+    let tempValue = { ...value };
+    let arr = [];
+    tempValue.options.map(item => {
+      arr.push(item.value);
+    })
+    if (arr) {
+      arr = arr.filter(item => item)
+      arr = [...new Set(arr)];
+      tempValue.options = arr
+    }
+    onFinish?.(tempValue);
+  }
+
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={handleFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="options" wrapperCol={{ span: 24 }}>
+        <SelectItem />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+function DDMultiSelectField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  const handleFinish = value => {
+    let tempValue = { ...value };
+    let arr = [];
+    tempValue.options.map(item => {
+      arr.push(item.value);
+    })
+    if (arr) {
+      arr = arr.filter(item => item)
+      arr = [...new Set(arr)];
+      tempValue.options = arr
+    }
+    onFinish?.(tempValue);
+  }
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={handleFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="选项" name="options" wrapperCol={{ span: 24 }}>
+        <SelectItem />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}
+
+function SelectItem(props) {
+  const { value, onChange } = props;
+  const [localValue, setLocalValue] = useState([]);
+  const pushItem = item => {
+    let tempValue = [...localValue, {
+      id: +new Date(),
+      value: item
+    }];
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+
+  const handleDelete = index => {
+    let tempValue = [...localValue];
+    tempValue.splice(index, 1);
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+  const handleInputOnChange = (targetValue, index) => {
+    let tempValue = [...localValue];
+    tempValue[index].value = targetValue;
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }
+  useEffect(() => {
+    let tempValue = value.map(item => ({ id: +new Date(), value: item }));
+    setLocalValue(tempValue);
+    onChange(tempValue);
+  }, []);
+
+  return (
+    <div style={{
+      minHeight: 40,
+      display: 'flex',
+      flexDirection: 'column'
+    }}>
+      <div>
+        <div style={{
+          fontSize: 4,
+          color: '#40a9ff',
+          cursor: 'pointer',
+          lineHeight: '32px'
+        }} onClick={() => { pushItem('') }}>添加选项</div>
+      </div>
+      <div style={{ minHeight: 20 }}>
+        {
+          localValue.map((item, index) =>
+            <div style={{
+              display: 'flex',
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 5
+            }}
+              key={item.id}
+            >
+              <Input style={{ marginRight: 10 }} value={item.value} onChange={e => handleInputOnChange(e.target.value, index)} />
+              <DeleteOutlined
+                onClick={() => handleDelete(index)}
+              />
+            </div>)
+        }
+      </div>
+    </div >
+  )
+}
+
+function NumberField(props) {
+  const { item, btns, onFinish } = props;
+  const [form] = Form.useForm();
+  return (
+    <Form form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} autoComplete="off" initialValues={item.props} onFinish={onFinish}>
+      <Form.Item label="标题" name="label">
+        <Input />
+      </Form.Item>
+      <Form.Item label="提示文字" name="placeholder">
+        <Input />
+      </Form.Item>
+      <Form.Item label="单位" name="unit">
+        <Input />
+      </Form.Item>
+      <Form.Item label="必填" valuePropName="checked" name="required">
+        <Switch />
+      </Form.Item>
+      {btns}
+    </Form>
+  );
+}

+ 80 - 0
src/components/AuditForm/constant.js

@@ -0,0 +1,80 @@
+import {
+  UserOutlined,
+  TeamOutlined,
+  ItalicOutlined,
+  FontSizeOutlined,
+  BorderOutlined,
+  BlockOutlined,
+  FieldNumberOutlined,
+} from '@ant-design/icons';
+
+export const COMPONENT_LIST = [
+  {
+    componentName: 'InnerContactField',
+    icon: <UserOutlined />,
+    props: {
+      label: '联系人',
+      placeholder: '请选择联系人',
+      required: false,
+      choice: '0'
+    },
+  },
+  {
+    componentName: 'DepartmentField',
+    icon: <TeamOutlined />,
+    props: {
+      label: '选择部门',
+      placeholder: '请选择部门',
+      required: false,
+      choice: '0'
+    },
+  },
+  {
+    componentName: 'TextField',
+    icon: <ItalicOutlined />,
+    props: {
+      label: '单行输入框',
+      placeholder: '请输入',
+      required: false,
+    },
+  },
+  {
+    componentName: 'TextareaField',
+    icon: <FontSizeOutlined />,
+    props: {
+      label: '多行输入框',
+      placeholder: '请输入',
+      required: false,
+    },
+  },
+  {
+    componentName: 'DDSelectField',
+    icon: <BorderOutlined />,
+    props: {
+      label: '单选框',
+      placeholder: '请选择',
+      required: false,
+      options: [],
+    },
+  },
+  {
+    componentName: 'DDMultiSelectField',
+    icon: <BlockOutlined />,
+    props: {
+      label: '多选框',
+      placeholder: '请选择',
+      require: false,
+      options: [],
+    },
+  },
+  {
+    componentName: 'NumberField',
+    icon: <FieldNumberOutlined />,
+    props: {
+      label: '数字输入框',
+      placeholder: '请输入',
+      required: false,
+      unit: ''
+    },
+  },
+];

+ 71 - 0
src/components/AuditForm/index.js

@@ -0,0 +1,71 @@
+import React, { useState, useEffect } from 'react';
+import FormContent from './FormContent';
+import ComponentLibrary from './ComponentLibrary';
+import ItemAttribute from './ItemAttribute';
+import { uuidv4 } from '@antv/xflow';
+import { Button } from 'antd';
+
+function AuditForm(props) {
+  const { value, onChange } = props;
+  const [formList, setFormList] = useState([]);
+  const [select, setSelect] = useState(-1);
+  const [visible, setVisible] = useState(false);
+
+  const handleAddItem = item => {
+    const formItem = generateItem(item);
+    handleChangeList([...formList, formItem]);
+    setVisible(false);
+  };
+
+  const generateItem = item => {
+    let newItem = {
+      ...item,
+      props: { ...item.props, id: `${item.componentName}_${uuidv4()}` },
+    };
+    delete newItem.icon;
+    return newItem;
+  };
+
+  const onChangeAttribute = newItem => {
+    let oldValue = formList[select].props;
+    formList[select].props = { ...oldValue, ...newItem };
+    handleChangeList([...formList]);
+  };
+
+  const handleChangeList = list => {
+    setFormList(list);
+    onChange?.(list);
+  };
+
+  useEffect(() => {
+    if (value instanceof Array) {
+      setFormList([...value]);
+      onChange?.([...value]);
+    }
+  }, [value]);
+
+  return (
+    <div>
+      <Button onClick={() => setVisible(true)}>增加控件</Button>
+      <div
+        style={{
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'flex-start',
+          width: 800,
+          marginTop: 20,
+        }}
+      >
+        <FormContent onSelect={setSelect} onChange={handleChangeList} list={formList}></FormContent>
+        <ItemAttribute item={formList[select]} onChange={onChangeAttribute}></ItemAttribute>
+      </div>
+      <ComponentLibrary
+        onOk={handleAddItem}
+        visible={visible}
+        onCancel={() => setVisible(false)}
+      ></ComponentLibrary>
+    </div>
+  );
+}
+
+export default AuditForm;

+ 384 - 0
src/components/ChartUtils/utils.js

@@ -0,0 +1,384 @@
+import moment from 'moment';
+
+import {
+  getDeviceRealData,
+  getDeviceRealDataByTime,
+  queryFormCurrentData,
+  queryFormHistoryData,
+} from '@/services/ProjectAdmin';
+
+const computePrefixExpression = (function() {
+  var s1 = [],
+    result;
+  return function(prefixExpression) {
+    s1.length = 0;
+    //计算
+    while (prefixExpression.length > 0) {
+      var o = prefixExpression.shift();
+      if (operator_map.indexOf(o) == -1) {
+        s1.push(Number(o));
+      } else {
+        switch (o) {
+          case '+': {
+            result = s1.pop() + s1.pop();
+            break;
+          }
+          case '-': {
+            result = s1.pop() - s1.pop();
+            break;
+          }
+          case '*': {
+            result = s1.pop() * s1.pop();
+            break;
+          }
+          case '/': {
+            result = s1.pop() / s1.pop();
+            break;
+          }
+        }
+        s1.push(result);
+      }
+    }
+    return s1[0];
+  };
+})();
+
+export async function getOptions(values, datas, formula, projectId) {
+  let allDatas = getAllParams(datas, formula);
+
+  let optionsData = getOptionsData(datas, formula, values);
+
+  // 请求接口
+  const { plcData, formData } = await queryData(values, allDatas, projectId);
+
+  optionsData = optionsData.map(item => {
+    const paramsInfo = item.paramsInfo;
+    let res;
+    if (paramsInfo.data_type == 0) {
+      // plc数据
+      res = findPlcData(paramsInfo, plcData, values.timeType);
+    } else if (paramsInfo.data_type == 1) {
+      // form数据
+      res = findFormData(paramsInfo, formData, values.timeType);
+    } else if (paramsInfo.data_type == 2) {
+      // 获取公式的值
+      res = getFormulaData(item.paramsInfo, plcData, formData, values.timeType);
+    }
+    return { ...item, ...res };
+  });
+  return {
+    ...values,
+    data: optionsData,
+  };
+}
+
+// 根据表单的数据项获取请求参数
+function getFormParams(datas, projectId) {
+  let params = {};
+  datas.forEach(item => {
+    if (!params[item.data_name]) params[item.data_name] = [];
+    params[item.data_name].push(item.data_title);
+  });
+  return Object.keys(params).map(data_name => ({
+    formName: data_name,
+    titles: params[data_name],
+    projectId,
+  }));
+}
+
+async function queryData(values, datas, projectId) {
+  let plcDatas = [],
+    formDatas = [];
+  datas.forEach(item => {
+    if (item.data_type == 0) {
+      plcDatas.push(item);
+    } else {
+      formDatas.push(item);
+    }
+  });
+
+  // 根据params获取form的数据
+  var formData = await getFormData(values, formDatas, projectId);
+  var plcData = await getPlcData(values, plcDatas);
+  if (values.timeType) {
+    // 获得时间轴全集
+    let times = getTimes(plcData[0]?.data, formData[0]?.data);
+
+    // 根据时间全集补填数据
+    formatData(plcData, times);
+    formatData(formData, times);
+  }
+  return {
+    plcData,
+    formData,
+  };
+}
+
+// 根据时间进行格式化数据
+function formatData(datas, times) {
+  datas.forEach(item => {
+    // 默认数据
+    let newItemData = [];
+    let i = 0;
+    times.forEach(t => {
+      if (item.data[i]?.htime != t) {
+        // 空缺时间要补0
+        newItemData.push({
+          htime: t,
+          val: 0,
+        });
+      } else {
+        newItemData.push(item.data[i]);
+        i++;
+      }
+    });
+    // 使用新值
+    item.data = newItemData;
+  });
+}
+function getTimes(plcTimes = [], formTimes = []) {
+  let times = {};
+
+  plcTimes.forEach(item => {
+    times[item.htime] = true;
+  });
+  formTimes.forEach(item => {
+    times[item.htime] = true;
+  });
+  return Object.keys(times).sort((a, b) => new Date(a) - new Date(b));
+}
+async function getFormData(values, datas, projectId) {
+  let arrtData = values.data || [];
+  const params = getFormParams(datas, projectId);
+  if (!values.timeType) {
+    // 请求最新数据
+    return await getFormCurrentData(params, values);
+  } else {
+    // 请求历史数据
+    return await getFormHistoryData(params, values);
+  }
+}
+// 请求表单最新数据
+async function getFormCurrentData(params, values) {
+  const { timeType, date } = values;
+  let data = [];
+  for (let i = 0; i < params.length; i++) {
+    const resData = await queryFormCurrentData(params[i]);
+    data = [...data, ...resData];
+  }
+  return data;
+}
+
+// 请求表单历史数据
+async function getFormHistoryData(params, values) {
+  const { timeType, date } = values;
+  let sTime, eTime;
+  let data = [];
+  // -1为自选日期  从date内获取时间
+  if (timeType == -1) {
+    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
+    eTime = moment(date[1])
+      .set(clear)
+      .format('YYYY-MM-DD HH:mm:ss');
+    sTime = moment(date[0])
+      .set(clear)
+      .format('YYYY-MM-DD HH:mm:ss');
+  } else {
+    let currentDate = moment();
+    eTime = currentDate.format('YYYY-MM-DD HH:mm:ss');
+    sTime = currentDate.add(-1 * timeType, 'hour').format('YYYY-MM-DD HH:mm:ss');
+  }
+
+  for (let i = 0; i < params.length; i++) {
+    const resData = await queryFormHistoryData({ ...params[i], eTime, sTime });
+    data = [...data, ...resData];
+  }
+  return data;
+}
+async function getPlcData(values, datas) {
+  let arrtData = values.data || [];
+  let params = getSingleData(datas);
+  if (!values.timeType) {
+    let res = await getData(params, values);
+    return res.data;
+  } else {
+    let singleData = [];
+    for (let i = 0; i < params.length; i++) {
+      const item = params[i];
+      let res = await getData(item, values);
+      singleData.push({
+        paramsInfo: item,
+        name: item.deviceName,
+        data: (res.data || []).map(item => {
+          return {
+            val: Number(item.val),
+            htime: moment(item.htime_at).format('YYYY-MM-DD HH:mm:ss'),
+          };
+        }),
+      });
+    }
+    return singleData;
+  }
+}
+
+// 根据数据项获取请求参数
+function getSingleData(datas) {
+  let params = [];
+  datas.forEach(({ device_id, device_items, seq }) => {
+    if (device_id && device_items) {
+      params.push({
+        deviceName: seq,
+        deviceId: device_id,
+        deviceItems: device_items,
+      });
+    }
+  });
+  return params;
+}
+function findPlcData(paramsInfo, plcData, timeType) {
+  // 实时数据与历史数据结构不一致  需判断
+  if (timeType) {
+    return plcData.find(resItem => resItem.paramsInfo.deviceName == paramsInfo.seq);
+  } else {
+    let res = plcData.find(resItem => resItem.alias == paramsInfo.seq);
+    return {
+      name: res.alias,
+      value: res.val,
+    };
+  }
+}
+function findFormData(paramsInfo, formData, timeType) {
+  // 实时数据与历史数据结构不一致  需判断
+  if (timeType) {
+    return formData.find(resItem => resItem.name == paramsInfo.data_title);
+  } else {
+    let res = formData.find(resItem => resItem.title == paramsInfo.data_title);
+    return {
+      name: res.title,
+      value: res.value,
+    };
+  }
+}
+
+function getAllParams(datas, formula) {
+  let allDatas = [...datas];
+  formula.forEach(f => {
+    f.params.forEach(params => {
+      if (params.data_type == 0) {
+        if (!datas.find(item => item.seq == params.seq)) {
+          allDatas.push(params);
+        }
+      } else {
+        if (!datas.find(item => item.data_title == params.data_title)) {
+          allDatas.push(params);
+        }
+      }
+    });
+  });
+  return allDatas;
+}
+
+function getOptionsData(datas, formula, values) {
+  let valuesData = values.data || [];
+
+  let optionsData = [];
+  formula.forEach((item, index) => {
+    var arrData = valuesData[index] || {};
+    item.data_type = 2;
+    arrData.paramsInfo = item;
+    optionsData.push(arrData);
+  });
+  datas.forEach((data, index) => {
+    var arrData = valuesData[index + formula.length] || {};
+    arrData.paramsInfo = data;
+    optionsData.push(arrData);
+  });
+  return optionsData;
+}
+
+var DATA_CACHE = {};
+// 请求plc数据
+async function getData(params, values) {
+  const { timeType, date, size, interval, aggregator } = values;
+  let key, etime, stime;
+  if (!timeType) {
+    key = `${params.map(item => item.deviceItems).join(',')}-${timeType}`;
+  } else if (timeType != -1) {
+    key = `${params.deviceItems}-${timeType}-${size}-${interval}-${aggregator}`;
+  } else {
+    let clear = { hour: 0, minute: 0, second: 0, millisecond: 0 };
+    etime = moment(date[1]).set(clear) * 1;
+    stime = moment(date[0]).set(clear) * 1;
+    key = `${params.deviceItems}-${stime}-${etime}-${size}-${interval}-${aggregator}`;
+  }
+  if (!DATA_CACHE[key]) {
+    if (!timeType) {
+      DATA_CACHE[key] = await getDeviceRealData(params);
+    } else {
+      if (timeType != -1) {
+        let currentDate = moment();
+        etime = currentDate * 1;
+        stime = currentDate.add(-1 * timeType, 'hour') * 1;
+      }
+      DATA_CACHE[key] = await getDeviceRealDataByTime({
+        deviceid: params.deviceId * 1,
+        dataitemid: params.deviceItems,
+        stime,
+        etime,
+        size,
+        interval,
+        aggregator,
+      });
+    }
+  }
+  return DATA_CACHE[key];
+}
+
+function getFormulaData(formula, plcData, formData, timeType) {
+  let expression = [...formula.expression];
+  let resDatas = formula.params.map(params => {
+    let res;
+    if (params.data_type == 0) {
+      res = findPlcData(params, plcData, timeType);
+      return {
+        ...params,
+        ...res,
+      };
+    } else {
+      res = findFormData(params, formData, timeType);
+      return {
+        ...params,
+        ...res,
+      };
+    }
+  });
+
+  if (timeType) {
+    let optionsData = [];
+    // 获取时间
+    let time = resDatas[0].data.map(item => item.htime);
+    time.forEach((htime, index) => {
+      resDatas.forEach(params => {
+        // 根据index去替换表达式中对应的值
+        expression[params.index] = params.data[index].val || 0;
+      });
+      optionsData.push({
+        htime: moment(htime).format('YYYY-MM-DD HH:mm:ss'),
+        val: computePrefixExpression([...expression]),
+      });
+    });
+    return {
+      data: optionsData,
+      name: formula.FormulaName,
+    };
+  } else {
+    resDatas.forEach(params => {
+      // 根据index去替换表达式中对应的值
+      expression[params.index] = params.value || 0;
+    });
+    return {
+      value: computePrefixExpression([...expression]),
+      name: formula.FormulaName,
+    };
+  }
+}

+ 36 - 0
src/components/DDComponents/DDAttachment/index.js

@@ -0,0 +1,36 @@
+import React, { useState, useEffect } from 'react';
+import { Upload, Button, message } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+const CORP_ID = 'ding0cdce2d5dbf986d9';
+const AGENT_ID = '1788653353';
+
+function DDAttachment(props) {
+  const { disabled, onChange, spaceId = '213865445' } = props;
+  const [value, setValue] = useState([]);
+
+  const upload = () => {
+    // dd.biz.util.uploadAttachment({
+    //   // image: { multiple: true, compress: false, max: 9, spaceId: '12345' },
+    //   space: { corpId: CORP_ID, spaceId: spaceId, isCopy: 1, max: 9 },
+    //   file: { spaceId: spaceId, max: 1 },
+    //   types: ['file', 'space'], //PC端支持["photo","file","space"]
+    //   onSuccess: function(result) {
+    //     //onSuccess将在文件上传成功之后调用
+    //     onChange?.(result.data);
+    //   },
+    //   onFail: function(err) {
+    //     console.log(err);
+    //     message.error('附件上传失败');
+    //   },
+    // });
+  };
+
+  return (
+    <div onClick={upload}>
+      <Button icon={<PlusOutlined />}>添加附件</Button>
+    </div>
+  );
+}
+
+export default DDAttachment;

+ 49 - 0
src/components/DDComponents/DDCode/index.js

@@ -0,0 +1,49 @@
+import QRCode from 'qrcode.react';
+import React, { useEffect, useState } from 'react';
+import { Spin } from 'antd';
+import { connect } from 'dva';
+
+let timer;
+function DDCode(props) {
+  const {
+    value,
+    onChange,
+    currentUser: { DingUserId },
+  } = props;
+  const [loading, setLoading] = useState(false);
+  // const [time, setTime] = useState(0);
+
+  // const queryCode = async () => {
+  //   setLoading(true);
+  //   // TODO 查询接口获得code
+
+  //   setLoading(false);
+  //   clearInterval(timer);
+  //   onChange({
+  //     code: '',
+  //   });
+  //   let second = 60 * 5;
+  //   if (second <= 0) return;
+  //   setTime(second);
+
+  //   timer = setInterval(() => {
+  //     setTime(time => {
+  //       if (time > 0) {
+  //         return time--;
+  //       }
+  //       return time;
+  //     });
+  //   }, second * 1000);
+  // };
+  if (!DingUserId) return <div>未知用户,请联系管理员。</div>;
+  return (
+    <div>
+      <Spin spinning={loading}>
+        <QRCode size={128} value={`http://47.96.12.136:8896/#/bom/dd-login/${DingUserId}`} />
+        请使用钉钉扫码授权
+      </Spin>
+    </div>
+  );
+}
+
+export default connect(({ user }) => ({ currentUser: user.currentUser }))(DDCode);

+ 13 - 0
src/components/DDComponents/DDDateField/index.js

@@ -0,0 +1,13 @@
+import React from 'react';
+import { DatePicker } from 'antd';
+
+function DDDateField(props) {
+  const { format, disabled, onChange } = props;
+
+  const handleChange = date => {
+    onChange?.(date.format('YYYY-MM-DD'));
+  };
+  return <DatePicker disabled={disabled} format={format.replace("yyyy","YYYY").replace("dd","DD")} onChange={handleChange} />;
+}
+
+export default DDDateField;

+ 22 - 0
src/components/DDComponents/DDDateRangeField/index.js

@@ -0,0 +1,22 @@
+import React from 'react';
+import { DatePicker } from 'antd';
+
+const { RangePicker } = DatePicker;
+
+function DDDateRangeField(props) {
+  const { format, disabled, onChange } = props;
+
+  const handleChange = date => {
+    let value = [date[0].format('YYYY-MM-DD'), date[1].format('YYYY-MM-DD')];
+    onChange?.(JSON.stringify(value));
+  };
+  return (
+    <RangePicker
+      disabled={disabled}
+      format={format.replace('yyyy', 'YYYY').replace('dd', 'DD')}
+      onChange={handleChange}
+    />
+  );
+}
+
+export default DDDateRangeField;

+ 30 - 0
src/components/DDComponents/DDMultiSelectField/index.js

@@ -0,0 +1,30 @@
+import React from 'react';
+import { Select } from 'antd';
+
+const { Option } = Select;
+
+function DDMultiSelectField(props) {
+  const { options, disabled, onChange } = props;
+
+  return (
+    <Select
+      mode="multiple"
+      allowClear
+      style={{ width: '100%' }}
+      disabled={disabled}
+      onChange={value => {
+        onChange(value);
+      }}
+    >
+      {options?.map(cur => {
+        return (
+          <Option key={cur} value={cur}>
+            {cur}
+          </Option>
+        );
+      })}
+    </Select>
+  );
+}
+
+export default DDMultiSelectField;

+ 107 - 0
src/components/DDComponents/DDPhotoField/AliyunOssUploader.js

@@ -0,0 +1,107 @@
+import React from 'react'
+import { Upload, message, Button} from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+
+class AliyunOSSUpload extends React.Component {
+  state = {
+    OSSData: {},
+  };
+
+  componentDidMount() {
+    this.init();
+  }
+
+  init = () => {
+    try {
+      const { OSSData } = this.props;
+
+      this.setState({
+        OSSData,
+      });
+    } catch (error) {
+      message.error(error);
+    }
+  };
+
+  onChange = ({ file, fileList }) => {
+    const { onChange, onDone , onUploading} = this.props;
+    console.log('Aliyun OSS:', file, fileList);
+    if (onChange) {
+      onChange([...fileList]);
+    }
+
+    if (onDone) {
+      if (file.status === 'done')
+        onDone(file)
+    }
+    if(onUploading && file.status === 'uploading') {
+      onUploading({ file, fileList })
+    }
+    this.setState({ fileList: [...fileList] });
+  };
+
+  onRemove = file => {
+    const { value, onChange } = this.props;
+
+    const files = value.filter(v => v.url !== file.url);
+
+    if (onChange) {
+      onChange(files);
+    }
+  };
+
+  transformFile = file => {
+    const { OSSData } = this.state;
+    file.url = OSSData.dir + file.name;
+    return file;
+  };
+
+  getExtraData = file => {
+    const { OSSData } = this.state;
+
+    return {
+      key: file.url,
+      OSSAccessKeyId: OSSData.accessid,
+      policy: OSSData.policy,
+      Signature: OSSData.signature,
+    };
+  };
+
+  beforeUpload = async () => {
+    const { OSSData } = this.state;
+    const expire = OSSData.expire * 1000;
+
+    if (expire < Date.now()) {
+      await this.init();
+    }
+    return true;
+  };
+
+  render() {
+    const { value, directory, label, noStyle, showUploadList, accept } = this.props;
+    const props = {
+      name: 'file',
+      fileList: this.state.fileList,
+      action: this.state.OSSData.host,
+      onChange: this.onChange,
+      onRemove: this.onRemove,
+      transformFile: this.transformFile,
+      data: this.getExtraData,
+      beforeUpload: this.beforeUpload,
+      accept: accept,
+      showUploadList: showUploadList !== false,
+      headers: { "Access-Control-Allow-Origin": "*" }
+    };
+    return (
+      <Upload {...props} directory={directory}>
+        {noStyle ? label : (<Button type="primary">
+          <PlusOutlined /> {label}
+        </Button>)}
+
+      </Upload>
+    );
+  }
+}
+
+
+export default AliyunOSSUpload;

+ 46 - 0
src/components/DDComponents/DDPhotoField/index.js

@@ -0,0 +1,46 @@
+import React, { useState, useEffect } from 'react';
+import { Upload, Button, Image } from 'antd';
+import AliyunOssUploader from '@/components/OssUpload/AliyunOssUploader';
+import { queryOSSData } from '@/services/boom';
+
+function DDPhotoField(props) {
+  const { disabled, onChange } = props;
+  const [value, setValue] = useState([]);
+  const [ossData, setOssData] = useState();
+
+  const OnModelFileDone = file => {
+    var path = ossData.host + '/' + file.url;
+    let newValue = [...value, path];
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+  const OnUploading = file => {};
+
+  useEffect(() => {
+    queryOSSData().then(res => {
+      setOssData(res.data);
+    });
+  }, []);
+
+  return ossData ? (
+    <div>
+      <div>
+        {value.map(img => (
+          <Image height={200} src={img} />
+        ))}
+      </div>
+      <AliyunOssUploader
+        OSSData={ossData}
+        onDone={OnModelFileDone}
+        onUploading={OnUploading}
+        noStyle={false}
+        showUploadList={false}
+        directory={false}
+        accept={'.png, .jpg, .jpeg'}
+        label="上传图片"
+      ></AliyunOssUploader>
+    </div>
+  ) : null;
+}
+
+export default DDPhotoField;

+ 28 - 0
src/components/DDComponents/DDSelectField/index.js

@@ -0,0 +1,28 @@
+import React from 'react';
+import { Select } from 'antd';
+
+const { Option } = Select;
+
+function DDSelectField(props) {
+  const { options, disabled, onChange } = props;
+
+  return (
+    <Select
+      style={{ width: '100%' }}
+      disabled={disabled}
+      onChange={value => {
+        onChange(String(value));
+      }}
+    >
+      {options?.map(cur => {
+        return (
+          <Option key={cur} value={cur}>
+            {cur}
+          </Option>
+        );
+      })}
+    </Select>
+  );
+}
+
+export default DDSelectField;

+ 75 - 0
src/components/DDComponents/DepartmentField/index.js

@@ -0,0 +1,75 @@
+import { TreeSelect } from 'antd';
+import React, { useState, useEffect } from 'react';
+import { queryDDdepList } from '@/services/boom';
+import { connect } from 'dva';
+
+function DepartmentField(props) {
+  const { value, onChange, depUserTree } = props;
+
+  // const [value, setValue] = useState();
+  const [treeData, setTreeData] = useState([]);
+
+  const genTreeNode = dep => {
+    return {
+      id: dep.dept_id,
+      pId: dep.parent_id,
+      value: dep.dept_id,
+      title: dep.name,
+      isLeaf: false,
+    };
+  };
+
+  const onLoadData = async ({ id }) => {
+    let depList = await queryDDdepList({ dept_id: id });
+
+    console.log(depList);
+    if (depList.length > 0) {
+      let nodes = depList.map(genTreeNode);
+      setTreeData([...treeData, ...nodes]);
+    }
+  };
+
+  const onChangeValue = newValue => {
+    console.log(newValue);
+    let dep = depUserTree.find(dep => dep.id == newValue);
+    onChange(String(dep?.ID));
+    // onChange({ value: dep?.title, id: dep?.ID });
+  };
+
+  // useEffect(() => {
+  //   onLoadData({});
+  // }, []);
+
+  return (
+    // <TreeSelect
+    //   treeDataSimpleMode
+    //   style={{
+    //     width: '100%',
+    //   }}
+    //   // value={value}
+    //   dropdownStyle={{
+    //     maxHeight: 400,
+    //     overflow: 'auto',
+    //   }}
+    //   placeholder="请选择部门"
+    //   onChange={onChangeValue}
+    //   loadData={onLoadData}
+    //   treeData={treeData}
+    // />
+    <TreeSelect
+      showSearch
+      multiple
+      allowClear
+      dropdownStyle={{
+        maxHeight: 400,
+        overflow: 'auto',
+      }}
+      style={{ width: '100%' }}
+      placeholder="请选择部门"
+      treeData={depUserTree}
+      onChange={onChangeValue}
+    />
+  );
+}
+
+export default connect(({ user }) => ({ depUserTree: user.depUserTree }))(DepartmentField);

+ 29 - 0
src/components/DDComponents/InnerContactField/index.js

@@ -0,0 +1,29 @@
+import React from 'react';
+import { message, Select } from 'antd';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+function InnerContactField(props) {
+  const { value, userList, onChange } = props;
+  return (
+    <Select
+      showSearch
+      onChange={value => {
+        onChange(String(value));
+        // onChange(JSON.stringify([value]));
+      }}
+      filterOption={(input, option) => option.children.toLowerCase().includes(input.toLowerCase())}
+    >
+      {(userList || []).map(item => (
+        <Option key={item.ID} value={item.ID}>
+          {item.CName}
+        </Option>
+      ))}
+    </Select>
+  );
+}
+
+export default connect(({ user }) => ({
+  userList: user.list,
+}))(InnerContactField);

+ 19 - 0
src/components/DDComponents/NumberField/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import { InputNumber } from 'antd';
+
+function NumberField(props) {
+  const { onChange, disabled, unit } = props;
+
+  return (
+    <InputNumber
+      style={{ width: '100%' }}
+      disabled={disabled}
+      formatter={value => `${value}${unit || ''}`}
+      onChange={e => {
+        onChange?.(String(e));
+      }}
+    />
+  );
+}
+
+export default NumberField;

+ 19 - 0
src/components/DDComponents/PhoneField/index.js

@@ -0,0 +1,19 @@
+import React from 'react';
+import { Input, Row, Col, Select } from 'antd';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+function PhoneField(props) {
+  const { onChange } = props;
+  return (
+    <Input.Group size="large">
+      <Select defaultValue="+86">
+        <Option value="+86">+86</Option>
+      </Select>
+      <Input style={{ width: '80%' }} {...props} onChange={e => onChange(e.target.value)} />
+    </Input.Group>
+  );
+}
+
+export default PhoneField;

+ 90 - 0
src/components/DDComponents/TableField/index.js

@@ -0,0 +1,90 @@
+import React, { useMemo, useState, useEffect } from 'react';
+import { Button } from 'antd';
+import DDComponents from '../index';
+import style from './index.less';
+
+function TableField(props) {
+  const { item, onChange } = props;
+  const [value, setValue] = useState([]);
+
+  const getEmptyValue = () => {
+    let data = item.children.map(children => ({
+      name: children.props.id,
+      value: '',
+    }));
+    data.id = Math.random();
+    return data;
+  };
+  const getChildren = (item, data) => {
+    let cur = data.find(d => d.name == item.props.id);
+    const childrenChange = value => {
+      cur.value = value;
+      onChange(JSON.stringify(value));
+    };
+    return DDComponents({ item, onChange: childrenChange, value: cur.value });
+  };
+
+  // const localValue = useMemo(() => {
+  //   let v = [];
+  //   try {
+  //     v = JSON.parse(value);
+  //   } catch (error) {}
+  //   if (v.length == 0) {
+  //     let data = getEmptyValue();
+  //     v.push(data);
+  //   }
+  //   return v;
+  // }, [value]);
+
+  const addRow = () => {
+    let newValue = [...value, getEmptyValue()];
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+
+  const removeRow = index => {
+    let newValue = [...value];
+    newValue.splice(index, 1);
+
+    setValue(newValue);
+    onChange(JSON.stringify(newValue));
+  };
+  useEffect(() => {
+    setValue([getEmptyValue()]);
+  }, []);
+
+  return (
+    <div>
+      <table className={style.table}>
+        <thead>
+          <tr>
+            <th></th>
+            {item.children.map(item => (
+              <th>{item.props.label}</th>
+            ))}
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          {value.map((curItem, index) => (
+            <tr key={curItem.id}>
+              <td>{index + 1}</td>
+              {item.children.map(item => (
+                <td>{getChildren(item, curItem)}</td>
+              ))}
+              <td>
+                {value.length > 1 && <a onClick={() => removeRow(index)}>删除</a>}
+                {/* <a>复制</a> */}
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+      <Button style={{ marginTop: 20 }} onClick={addRow}>
+        {item.props.actionName}
+      </Button>
+    </div>
+  );
+}
+
+export default TableField;

+ 16 - 0
src/components/DDComponents/TableField/index.less

@@ -0,0 +1,16 @@
+.table {
+  width: 100%;
+  td {
+    padding: 10px;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+  }
+  th {
+    padding: 10px;
+    color: #000000d9;
+    font-weight: 500;
+    text-align: left;
+    background: #fafafa;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+    transition: background 0.3s ease;
+  }
+}

+ 124 - 0
src/components/DDComponents/index.js

@@ -0,0 +1,124 @@
+import { Input, InputNumber, Select, DatePicker, Rate } from 'antd';
+import TableField from './TableField';
+import PhoneField from './PhoneField';
+import InnerContactField from './InnerContactField';
+import DepartmentField from './DepartmentField';
+import DDMultiSelectField from './DDMultiSelectField';
+import NumberField from './NumberField';
+import DDPhotoField from './DDPhotoField';
+import DDSelectField from './DDSelectField';
+import DDDateField from './DDDateField';
+import DDDateRangeField from './DDDateRangeField';
+import DDAttachment from './DDAttachment';
+
+const { Option } = Select;
+const { RangePicker } = DatePicker;
+
+export default function DDComponents(props) {
+  const { item, onChange } = props;
+  const {
+    id,
+    label,
+    bizAlias,
+    required,
+    placeholder,
+    options,
+    align,
+    statField,
+    hideLabel,
+    objOptions,
+    format,
+    pushToAttendance,
+    labelEditableFreeze,
+    requiredEditableFreeze,
+    unit,
+    extract,
+    link,
+    payEnable,
+    bizType,
+    childFieldVisible,
+    notPrint,
+    verticalPrint,
+    hiddenInApprovalDetail,
+    disabled,
+    notUpper,
+    children, // 子控件
+  } = item.props;
+  let component = null;
+  switch (item.componentName) {
+    case 'TextField': //单行输入
+      component = (
+        <Input
+          disabled={disabled}
+          placeholder={placeholder}
+          onChange={e => onChange?.(e.target.value)}
+        />
+      );
+      break;
+    case 'TextareaField': //多行输入
+      component = (
+        <Input.TextArea
+          disabled={disabled}
+          placeholder={placeholder}
+          onChange={e => onChange?.(e.target.value)}
+        />
+      );
+      break;
+    case 'NumberField': //数字输入
+      component = <NumberField disabled={disabled} unit={unit} onChange={onChange} />;
+      break;
+    case 'DDSelectField': //单选框
+      component = <DDSelectField options={options} onChange={onChange} disabled={disabled} />;
+      break;
+    case 'DDMultiSelectField': //多选框
+      component = <DDMultiSelectField disabled={disabled} options={options} onChange={onChange} />;
+      break;
+    case 'DDDateField': //日期控件
+      component = <DDDateField format={format} disabled={disabled} onChange={onChange} />;
+      break;
+    case 'DDDateRangeField': //时间区间控件
+      component = <DDDateRangeField format={format} disabled={disabled} onChange={onChange} />;
+      break;
+    case 'TextNote': //文本说明控件
+      console.info('文本说明控件!');
+      console.log(item);
+      break;
+    case 'PhoneField': //电话控件
+      component = <PhoneField onChange={onChange} />;
+      break;
+    case 'DDPhotoField': //图片控件
+      component = <DDPhotoField />;
+      break;
+    case 'MoneyField': //金额控件
+      component = <Input placeholder={placeholder} onChange={onChange} />;
+      break;
+    case 'TableField': //明细控件
+      component = <TableField item={item} />;
+      break;
+    case 'DDAttachment': //附件
+      // component = <DDAttachment />
+      // component = '附件控件未渲染!'
+      console.info('附件控件未渲染!');
+      break;
+    case 'InnerContactField': //联系人控件
+      component = <InnerContactField onChange={onChange}></InnerContactField>;
+      break;
+    case 'DepartmentField': //部门控件
+      component = <DepartmentField onChange={onChange} />;
+      break;
+    case 'RelateField': //关联审批单
+      component = '关联审批单控件未渲染!'
+      break;
+    case 'AddressField': //省市区控件
+      component = '省市区控件未渲染!'
+      break;
+    case 'StarRatingField': //评分控件
+      component = '评分控件未渲染!'
+      break;
+    case 'FormRelateField': //关联控件
+      component = '关联控件未渲染!'
+      break;
+  }
+
+  return component;
+}

+ 944 - 0
src/components/DataMeter/config.js

@@ -0,0 +1,944 @@
+export const CHILD_MAP = {
+  ProjectInfo: [
+    {
+      title: '项目编号',
+      key: 'Code',
+      show: true,
+    },
+    {
+      title: '项目工期',
+      key: 'Duration',
+      show: true,
+    },
+    {
+      title: '建设单位',
+      key: 'ConstructionUnit',
+      show: true,
+    },
+    {
+      title: '承建单位',
+      key: 'UndertakenUnit',
+      show: true,
+    },
+    {
+      title: '所在地',
+      key: 'Position',
+      show: true,
+    },
+
+    {
+      title: '项目规模',
+      key: 'Scale',
+      show: true,
+    },
+    {
+      title: '主体工艺',
+      key: 'MainProcess',
+      show: true,
+    },
+    {
+      title: '服务范围',
+      key: 'ServiceScope',
+      show: true,
+    },
+    {
+      title: '签约时间',
+      key: 'ContractTime',
+      show: true,
+    },
+    {
+      title: '运营服务时间',
+      key: 'ServiceTime',
+      show: true,
+    },
+    {
+      title: '合作模式',
+      key: 'CooperateMode',
+      show: true,
+    },
+    {
+      title: '出水执行标准',
+      key: 'WaterStandard',
+      show: true,
+    },
+    {
+      title: '核心管理人员',
+      key: 'Admin',
+      show: true,
+    },
+    {
+      title: '项目成员',
+      key: 'User',
+      show: true,
+    },
+  ],
+  AlarmCenter: [
+    { title: '全部', key: 4, show: true },
+    {
+      title: '待办事项',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '异常报警',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '逾期预警',
+      key: 2,
+      show: true,
+    },
+  ],
+  MessageCenter: [
+    {
+      title: '消息推送',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '新闻动态',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '大事记',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '所获奖项',
+      key: 4,
+      show: true,
+    },
+  ],
+  PlanManagement: [
+    // {
+    //   title: '设计',
+    //   key: 1,
+    //   show: true,
+    // },
+    // {
+    //   title: '采购',
+    //   key: 2,
+    //   show: true,
+    // },
+    {
+      title: '到货',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '施工',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '单机调试',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '系统调试',
+      key: 4,
+      show: true,
+    },
+    {
+      title: '试运行',
+      key: 5,
+      show: true,
+    },
+    // {
+    //   title: '维修',
+    //   key: 6,
+    //   show: true,
+    // },
+    {
+      title: '项目动态',
+      key: 7,
+      show: true,
+    },
+    // {
+    //   title: '巡检',
+    //   key: 7,
+    //   show: true,
+    // },
+    // {
+    //   title: '其它',
+    //   key: 8,
+    //   show: true,
+    // },
+  ],
+  Other: [
+    {
+      title: '安全操作规范',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '项目日志',
+      key: 2,
+      show: true,
+    },
+  ],
+  Monitor: [],
+  DebugData: [],
+  DataCenter: [
+    {
+      title: '水质管理',
+      key: 1,
+      show: true,
+    },
+    {
+      title: '水质指标',
+      key: 2,
+      show: true,
+    },
+    {
+      title: '客户满意度',
+      key: 3,
+      show: true,
+    },
+    {
+      title: '设备完好率',
+      key: 4,
+      show: true,
+    },
+    {
+      title: 'KPI',
+      key: 5,
+      show: true,
+      children: [
+        {
+          title: '累计EBITDA',
+          key: 11,
+          show: true,
+        },
+        {
+          title: '累计经营性净现金流',
+          key: 12,
+          show: true,
+        },
+      ],
+    },
+    {
+      title: '水量计费',
+      key: 6,
+      show: true,
+    },
+    {
+      title: '预算成本',
+      key: 7,
+      show: true,
+      children: [
+        {
+          title: '累计成本支出',
+          key: 13,
+          show: true,
+        },
+        {
+          title: '累计月度吨水成本',
+          key: 14,
+          show: true,
+        },
+      ],
+    },
+    {
+      title: '季度绩效',
+      key: 8,
+      show: true,
+    },
+    {
+      title: '化学实验室数据',
+      key: 9,
+      show: true,
+    },
+    {
+      title: '安全生产天',
+      key: 10,
+      show: true,
+    },
+    // {
+    //   title: '调试数据',
+    //   key: 11,
+    //   show: true,
+    // },
+  ],
+};
+
+export const DEFAULT_LAYOUT = [
+  {
+    i: '41',
+    key: 'AlarmCenter',
+    w: 8,
+    h: 5,
+    x: 28,
+    y: 14,
+    child: [
+      { title: '全部', key: 4, show: true },
+      { title: '待办事项', key: 3, show: true },
+      { title: '异常报警', key: 1, show: true },
+      { title: '逾期预警', key: 2, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 2,
+  },
+  {
+    i: '40',
+    key: 'DataCenter',
+    active: 11,
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '39',
+    key: 'DataCenter',
+    active: '4',
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 9,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '38',
+    key: 'DataCenter',
+    active: '2',
+    w: 6,
+    h: 5,
+    x: 42,
+    y: 4,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '37',
+    key: 'DataCenter',
+    active: 10,
+    w: 6,
+    h: 4,
+    x: 42,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '36',
+    key: 'DataCenter',
+    active: '1',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 4,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '35',
+    key: 'DataCenter',
+    active: 13,
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '34',
+    key: 'DataCenter',
+    active: '8',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '33',
+    key: 'DataCenter',
+    active: 11,
+    w: 7,
+    h: 4,
+    x: 7,
+    y: 10,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '32',
+    key: 'DataCenter',
+    active: '2',
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 5,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '31',
+    key: 'DataCenter',
+    active: '1',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '30',
+    key: 'DataCenter',
+    w: 7,
+    h: 5,
+    x: 7,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: '累计EBITDA', key: 11, show: true },
+      { title: '累计经营性净现金流', key: 12, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '累计成本支出', key: 13, show: true },
+      { title: '累计月度吨水成本', key: 14, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 12,
+  },
+  {
+    i: '29',
+    key: 'DataCenter',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 14,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '20',
+    key: 'DataCenter',
+    w: 6,
+    h: 5,
+    x: 36,
+    y: 9,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '14',
+    key: 'DataCenter',
+    w: 7,
+    h: 5,
+    x: 0,
+    y: 5,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '12',
+    key: 'PlanManagement',
+    w: 6,
+    h: 4,
+    x: 36,
+    y: 0,
+    child: [
+      { title: '设计', key: 1, show: true },
+      { title: '采购', key: 2, show: true },
+      { title: '施工', key: 3, show: true },
+      { title: '调试', key: 4, show: true },
+      { title: '保养', key: 5, show: true },
+      { title: '维修', key: 6, show: true },
+      { title: '项目动态', key: 7, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 1,
+  },
+  {
+    i: '10',
+    key: 'PlanManagement',
+    w: 7,
+    h: 4,
+    x: 0,
+    y: 10,
+    child: [
+      { title: '设计', key: 1, show: true },
+      { title: '采购', key: 2, show: true },
+      { title: '施工', key: 3, show: true },
+      { title: '调试', key: 4, show: true },
+      { title: '保养', key: 5, show: true },
+      { title: '维修', key: 6, show: true },
+      { title: '项目动态', key: 7, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 2,
+  },
+  {
+    i: '8',
+    key: 'ProjectInfo',
+    w: 14,
+    h: 5,
+    x: 14,
+    y: 14,
+    active: 1,
+    child: [
+      { title: '项目编号', key: 'Code', show: true },
+      { title: '项目工期', key: 'Duration', show: true },
+      { title: '建设单位', key: 'ConstructionUnit', show: true },
+      { title: '承建单位', key: 'UndertakenUnit', show: true },
+      { title: '所在地', key: 'Position', show: true },
+      { title: '项目规模', key: 'Scale', show: true },
+      { title: '主体工艺', key: 'MainProcess', show: true },
+      { title: '服务范围', key: 'ServiceScope', show: true },
+      { title: '签约时间', key: 'ContractTime', show: true },
+      { title: '运营服务时间', key: 'ServiceTime', show: true },
+      { title: '合作模式', key: 'CooperateMode', show: true },
+      { title: '出水执行标准', key: 'WaterStandard', show: true },
+      { title: '核心管理人员', key: 'Admin', show: true },
+      { title: '项目成员', key: 'User', show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  { i: '7', key: 'Map', w: 22, h: 14, x: 14, y: 0, moved: false, static: false },
+];
+export const DEFAULT_MAP_LAYOUT = [
+  {
+    i: '20',
+    key: 'DataCenter',
+    w: 14,
+    h: 7,
+    x: 0,
+    y: 0,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '19',
+    key: 'DataCenter',
+    w: 7,
+    h: 7,
+    x: 7,
+    y: 13,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 9,
+  },
+  {
+    i: '18',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 0,
+    y: 13,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 3,
+  },
+  {
+    i: '17',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 7,
+    y: 7,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 7,
+  },
+  {
+    i: '15',
+    key: 'DataCenter',
+    w: 13,
+    h: 6,
+    x: 35,
+    y: 6,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 6,
+  },
+  {
+    i: '12',
+    key: 'DataCenter',
+    w: 7,
+    h: 6,
+    x: 0,
+    y: 7,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 5,
+  },
+  {
+    i: '11',
+    key: 'DataCenter',
+    w: 13,
+    h: 7,
+    x: 35,
+    y: 12,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+    active: 4,
+  },
+  { i: '7', key: 'Map', w: 21, h: 12, x: 14, y: 0, moved: false, static: false },
+  {
+    i: '5',
+    key: 'AlarmCenter',
+    w: 10,
+    h: 6,
+    x: 25,
+    y: 12,
+    active: 3,
+    child: [
+      { title: '待办事项', key: 3, show: true },
+      { title: '异常报警', key: 1, show: true },
+      { title: '项目预警', key: 2, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '3',
+    key: 'DataCenter',
+    w: 13,
+    h: 6,
+    x: 35,
+    y: 0,
+    active: 10,
+    child: [
+      { title: '水质管理', key: 1, show: true },
+      { title: '水质指标', key: 2, show: true },
+      { title: '客户满意度', key: 3, show: true },
+      { title: '设备完好率', key: 4, show: true },
+      { title: 'KPI', key: 5, show: true },
+      { title: '水量计费', key: 6, show: true },
+      { title: '预算成本', key: 7, show: true },
+      { title: '季度绩效', key: 8, show: true },
+      { title: '化学实验室数据', key: 9, show: true },
+      { title: '安全生产天', key: 10, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+  {
+    i: '2',
+    key: 'MessageCenter',
+    w: 11,
+    h: 6,
+    x: 14,
+    y: 12,
+    active: 1,
+    child: [
+      { title: '消息推送', key: 1, show: true },
+      { title: '新闻动态', key: 2, show: true },
+      { title: '大事记', key: 3, show: true },
+      { title: '所获奖项', key: 4, show: true },
+    ],
+    moved: false,
+    static: false,
+  },
+];
+
+export const U3D_PATH_STATE = {
+  ProjectInfo: 1,
+  AlarmCenter: {
+    3: 53,
+    1: 39,
+    2: 54,
+  },
+  MessageCenter: {
+    1: 54,
+  },
+  FileManagement: [27, 44],
+  Monitor: 100,
+  PlanManagement: {
+    1: 48,
+    2: 49,
+    3: 50,
+    4: 50,
+    5: 51,
+  },
+  Other: {
+    2: 35
+  },
+};
+
+export const COL_COLS = 48;
+export const ROW_COLS = 24;

+ 233 - 0
src/components/Flow/components/judgeComponent/index.tsx

@@ -0,0 +1,233 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { Checkbox, Select, TreeSelect } from 'antd';
+import { DeleteOutlined } from '@ant-design/icons';
+import { ComponentName, FormItem } from '../../node/judgeNode/mapServe';
+import { InputNumberFiled } from '../../node/fields';
+import { connect } from 'dva';
+
+const { Option } = Select;
+
+interface Condition {
+  smallValue?: number;
+  smallSign?: number;
+  bigValue?: number;
+  bigSign?: number;
+}
+
+export interface JudgeType {
+  type: string;
+  id: string;
+  condition?: Condition;
+  values?: any[];
+}
+
+export const JudgeOptions = [
+  { label: '小于', value: 1 },
+  { label: '大于', value: 2 },
+  { label: '小于等于', value: 3 },
+  { label: '等于', value: 4 },
+  { label: '大于等于', value: 5 },
+  { label: '介于(两个数之间)', value: 6 },
+];
+export const SiginOptions = [
+  { label: '<', value: 1 },
+  { label: '≤', value: 3 },
+];
+
+const RenderJudge = (props: any) => {
+  const { formItems = '', onChange, depUserTree } = props;
+  let formData: FormItem[] = formItems ? JSON.parse(formItems) : [];
+
+  const handleChange = (values: any[], item: FormItem, condition?: Condition) => {
+    const itemCur = formData.find(cur => cur.props.id == item.props.id);
+    let judge: JudgeType = {
+      id: item.props.id,
+      values:
+        values.length == 0 && itemCur && itemCur.judge?.values ? itemCur.judge.values : values,
+      type: item.componentName,
+      condition:
+        !condition && itemCur && itemCur.judge?.condition ? itemCur.judge.condition : condition,
+    };
+    const newItem: FormItem = {
+      ...item,
+      judge,
+    };
+    const index = formData.findIndex(cur => cur.props.id == item.props.id);
+    if (index != -1) {
+      formData[index] = newItem;
+    } else {
+      formData.push(newItem);
+    }
+    onChange(formData);
+  };
+
+  const getTreeNode = (pid, dep) => {
+    return {
+      id: dep.ID,
+      pId: pid,
+      value: dep.ID,
+      title: dep.Name,
+      isLeaf: false,
+    };
+  };
+  const getResult = (pid, items) => {
+    let result = [];
+    items.forEach(item => {
+      let node = getTreeNode(pid, item);
+      result.push(node);
+      if (item.children) {
+        let node = getResult(item.ID, item.children);
+        result = [...result, ...node];
+      }
+    });
+    return result;
+  };
+
+  const handleDelete = (item: FormItem) => {
+    const index = formData.findIndex(cur => cur.props.id == item.props.id);
+    if (index != -1) {
+      formData.splice(index, 1);
+      onChange(formData);
+    }
+  };
+
+  const handleTreeChange = (values: (string | number)[], item: FormItem) => {
+    const newValues = values.map(cur => {
+      if (typeof cur == 'string' && cur.includes('||')) {
+        return { type: 'user', value: Number(cur.split('||')[0]), origin: cur };
+      } else {
+        return { type: 'dep', value: cur, origin: cur };
+      }
+    });
+    handleChange(newValues, item);
+  };
+
+  const RenderComp = (item: FormItem) => {
+    let component;
+    switch (item.componentName) {
+      case ComponentName.Inner:
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>发起人</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <TreeSelect
+              showSearch
+              multiple
+              allowClear
+              defaultValue={item.judge?.values?.map(item => item.origin)}
+              style={{ width: '100%' }}
+              placeholder="请选择部门"
+              treeData={depUserTree}
+              onChange={values => {
+                handleTreeChange(values, item);
+                // handleChange(values, item)
+              }}
+            />
+          </>
+        );
+        break;
+      case ComponentName.Depart:
+        break;
+      case ComponentName.Money:
+      case ComponentName.Number:
+        const { judge, props } = item;
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>{props.label}</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <Select
+              options={JudgeOptions}
+              defaultValue={judge?.values[0]}
+              onChange={(value: number) => {
+                let newCondition: Condition = null;
+                if (judge && judge.values && judge.values.length > 0) {
+                  if (judge?.values[0] == 6 && value != 6) newCondition = {};
+                  else if (judge.values[0] != 6 && value == 6) newCondition = {};
+                }
+                handleChange([value], item, newCondition);
+              }}
+            />
+            {judge?.values[0] == 6 ? (
+              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                <InputNumberFiled
+                  value={judge?.condition?.smallValue}
+                  onChange={value => {
+                    handleChange([], item, { ...judge?.condition, smallValue: value });
+                  }}
+                />
+                <Select
+                  defaultValue={judge?.condition?.smallSign}
+                  onChange={(value: number) => {
+                    handleChange([], item, { ...judge?.condition, smallSign: Number(value) });
+                  }}
+                >
+                  {SiginOptions.map(item => (
+                    <Option key={item.value}>{item.label}</Option>
+                  ))}
+                </Select>
+                <span>N</span>
+                <Select
+                  defaultValue={judge?.condition?.bigSign}
+                  onChange={(value: number) => {
+                    handleChange([], item, { ...judge?.condition, bigSign: Number(value) });
+                  }}
+                >
+                  {SiginOptions.map(item => (
+                    <Option key={item.value}>{item.label}</Option>
+                  ))}
+                </Select>
+                <InputNumberFiled
+                  value={judge?.condition?.bigValue}
+                  onChange={value => {
+                    handleChange([], item, { ...judge?.condition, bigValue: value });
+                  }}
+                />
+              </div>
+            ) : (
+              <InputNumberFiled
+                value={judge?.condition?.smallValue}
+                onChange={value => {
+                  handleChange([], item, { smallValue: value });
+                }}
+              />
+            )}
+          </>
+        );
+        break;
+
+      case ComponentName.MultiSelect:
+      case ComponentName.Select:
+        const options = item.props.options;
+        component = (
+          <>
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <div>{item.props.label}</div>
+              <DeleteOutlined onClick={() => handleDelete(item)} />
+            </div>
+            <Checkbox.Group
+              defaultValue={item.judge?.values}
+              onChange={(values: any) => handleChange(values, item)}
+            >
+              {options.map((cur: any, idx: number) => {
+                return (
+                  <Checkbox key={`${cur}_${idx}`} value={cur}>
+                    {cur}
+                  </Checkbox>
+                );
+              })}
+            </Checkbox.Group>
+          </>
+        );
+        break;
+    }
+    return component;
+  };
+
+  return <>{formData.map(item => RenderComp(item))}</>;
+};
+
+export default connect(({ user }) => ({ depUserTree: user.depUserTree }))(RenderJudge);

+ 76 - 0
src/components/Flow/components/judgeModal/index.tsx

@@ -0,0 +1,76 @@
+import { Button, Checkbox, Modal } from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+import React, { useMemo, useState } from 'react';
+import { ComponentName, FormItem } from '../../node/judgeNode/mapServe';
+
+const AddCondition = (props: any) => {
+  const { items = [], formItems = '', onOk } = props;
+  const [visible, setVisible] = useState(false);
+  const [values, setValues] = useState([]);
+
+  let formData = useMemo(() => {
+    let formDat = formItems ? JSON.parse(formItems) : [];
+    setValues(formDat.map(cur => cur.props.id));
+    return formDat;
+  }, [formItems]);
+
+  const data: FormItem[] = items
+    .filter((item: FormItem) => {
+      return (
+        (item.componentName == ComponentName.Inner ||
+          // item.componentName == 'DepartmentField' ||
+          item.componentName == ComponentName.Select ||
+          item.componentName == ComponentName.MultiSelect ||
+          item.componentName == ComponentName.Number ||
+          item.componentName == ComponentName.Money) &&
+        item.props.required
+      );
+    })
+    .map(item => {
+      return {
+        label: item.props.label,
+        ...item,
+      };
+    });
+
+  const onChange = (values: []) => {
+    setValues(values);
+  };
+
+  const handleOk = () => {
+    let resultData: FormItem[] = [];
+    values.map(value => {
+      const item = formData.find(cur => cur.props.id == value);
+      if (item) {
+        resultData.push(item);
+      } else {
+        const item = items.find(cur => cur.props.id == value);
+        resultData.push(item);
+      }
+    });
+    onOk(resultData);
+    setVisible(false);
+  };
+
+  return (
+    <>
+      <div className={`group`}>
+        <Button icon={<PlusOutlined />} onClick={() => setVisible(true)}>
+          选择条件
+        </Button>
+        还有{data.length - values.length}组可用条件
+      </div>
+      <Modal title="选择条件" visible={visible} onOk={handleOk} onCancel={() => setVisible(false)}>
+        <p>请选择用来区分审批流程的条件字段 ,已选{values.length}个</p>
+        <Checkbox.Group value={values} onChange={onChange}>
+          {data.map((item: FormItem) => (
+            <Checkbox key={item.props.id} value={item.props.id}>
+              {item.props.label}
+            </Checkbox>
+          ))}
+        </Checkbox.Group>
+      </Modal>
+    </>
+  );
+};
+export default AddCondition;

+ 132 - 0
src/components/Flow/node/auditNode/index.tsx

@@ -0,0 +1,132 @@
+import React, { useMemo } from 'react';
+AuditServe;
+import AuditServe, { IDTYPE, TYPE } from './mapServe';
+import { connect } from 'dva';
+export { AuditServe };
+
+const CustomRect = props => {
+  const { size = { width: 130, height: 50 }, data, depUserTree, roleList, userList } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize, type = 2, initiator, audits } = data;
+
+  const contentText = useMemo(() => {
+    let text = '请选择审批人';
+    switch (type) {
+      case TYPE.AUDIT:
+        text = '请选择审批人';
+        break;
+      case TYPE.INITIATOR:
+        text = '所有人';
+        break;
+      case TYPE.COPYMAN:
+        text = '请选择抄送人';
+        break;
+    }
+    const getName = (id, data) => {
+      let name = '';
+      for (let i = 0; i < data.length; i++) {
+        let item = data[i];
+        if (item.ID == id) {
+          return item.title;
+        } else if (item.children?.length > 0) {
+          let title = getName(id, item.children);
+          if (title) return title;
+        }
+      }
+      return name;
+    };
+    if (type != TYPE.AUDIT) {
+      if (initiator?.length > 0) {
+        const list = initiator
+          .map(item => {
+            return getName(item.value, depUserTree);
+          })
+          .filter(item => item);
+        return list.join(',');
+      } else {
+        return text;
+      }
+    } else {
+      if (audits?.length > 0) {
+        return audits[0].type == IDTYPE.ROLE
+          ? roleList.find(item => item.ID == audits[0].value)?.Name
+          : userList.find(item => item.ID == audits[0].value)?.CName;
+        // return roleList.find(item => item.ID == audits[0].value)?.Name;
+      } else {
+        return text;
+      }
+    }
+  }, [initiator, type, audits, depUserTree, roleList]);
+
+  // const type: TYPE = 0;
+  const titleDom = () => {
+    let color = COLOR.AUDIT;
+    let text = label == '动作节点' ? TITLETEXT.AUDIT : label;
+    switch (type) {
+      case TYPE.AUDIT:
+        color = COLOR.AUDIT;
+        text = TITLETEXT.AUDIT;
+        break;
+      case TYPE.INITIATOR:
+        color = COLOR.INITIATOR;
+        text = TITLETEXT.INITIATOR;
+        break;
+      case TYPE.COPYMAN:
+        color = COLOR.COPYMAN;
+        text = TITLETEXT.COPYMAN;
+        break;
+    }
+    return (
+      <div
+        style={{
+          width: '100%',
+          height: `${fontSize + 16}px`,
+          paddingLeft: '6px',
+          backgroundColor: color,
+          color: 'white',
+          fontSize,
+        }}
+      >
+        {text}
+      </div>
+    );
+  };
+  return (
+    <div
+      style={{
+        width,
+        height,
+        boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)',
+      }}
+    >
+      {titleDom()}
+      <div
+        style={{
+          paddingLeft: '6px',
+          height: `${height - fontSize - 16}px`,
+          lineHeight: `${height - fontSize - 16}px`,
+          fontSize,
+        }}
+      >
+        {contentText}
+      </div>
+    </div>
+  );
+};
+
+export default connect(({ user }) => ({
+  depUserTree: user.depUserTree,
+  roleList: user.roleList,
+  userList: user.list,
+}))(CustomRect);
+
+const enum COLOR {
+  INITIATOR = '#576A95',
+  AUDIT = '#FF943E',
+  COPYMAN = '#3296FA',
+}
+const enum TITLETEXT {
+  INITIATOR = '发起人',
+  AUDIT = '审批人',
+  COPYMAN = '抄送人',
+}

+ 283 - 0
src/components/Flow/node/auditNode/mapServe.tsx

@@ -0,0 +1,283 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import { Position, Size, ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../fields';
+import { PREFIX } from '../constants';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+import { Button, Radio, RadioChangeEvent, Select, TreeSelect } from 'antd';
+
+const { Option } = Select;
+
+export const enum TYPE {
+  INITIATOR = 1,
+  AUDIT,
+  JUDGE,
+  COPYMAN,
+}
+
+export const typeOption = [
+  { label: '审批人', value: TYPE.AUDIT },
+  { label: '发起人', value: TYPE.INITIATOR },
+  { label: '抄送人', value: TYPE.COPYMAN },
+];
+
+export interface IConfig {
+  label?: string;
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  count?: number;
+  fontSize?: number;
+  fontFill?: string;
+  fill?: string;
+  stroke?: string;
+  flow_id?: string;
+  flow_node_id?: string;
+  process_code?: string;
+  type: TYPE;
+  initiator: { type: string; value: number; origin?: string | number; name?: string }[];
+  //审批人目前只支持单选角色或者单选人
+  audits: { type: string; value: number; origin?: string | number; name?: string }[];
+}
+
+export const enum IDTYPE {
+  DEP = 'dep',
+  USER = 'user',
+  ROLE = 'role',
+}
+
+const Component = (props: any) => {
+  const defaultConfig = {
+    type: TYPE.AUDIT,
+  };
+  const { config, plugin = {}, depUserTree, roleList, userList } = props;
+  const { updateNode } = plugin;
+  const [isRole, setIsRole] = useState(1); ///按角色还是个人
+
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...defaultConfig,
+    ...config,
+  });
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        type: nodeConfig.type ? nodeConfig.type : TYPE.AUDIT,
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        type: nodeConfig.type ? nodeConfig.type : TYPE.AUDIT,
+        ...value,
+      });
+    }
+  };
+
+  useEffect(() => {
+    const bool = nodeConfig.audits?.some(item => item.type == IDTYPE.USER) ? 0 : 1;
+    console.log(bool);
+    setIsRole(bool);
+  }, []);
+  console.log('===================', nodeConfig);
+
+  const handleTreeChange = (values: (string | number)[]) => {
+    const newValues = values.map(cur => {
+      if (typeof cur == 'string' && cur.includes('||')) {
+        return { type: IDTYPE.USER, value: Number(cur.split('||')[0]), origin: cur };
+      } else {
+        return { type: IDTYPE.DEP, value: cur, origin: cur };
+      }
+    });
+    onNodeConfigChange('initiator', newValues);
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+  useEffect(() => {
+    setNodeConfig({
+      ...defaultConfig,
+      ...config,
+    });
+  }, [config]);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+        <SelectField
+          label="节点类型"
+          value={nodeConfig.type}
+          onChange={value => {
+            onNodeConfigChange('type', value);
+          }}
+          options={typeOption}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>样式</h5>
+        <Position
+          x={nodeConfig.x}
+          y={nodeConfig.y}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <Size
+          width={nodeConfig.width}
+          height={nodeConfig.height}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <ColorPicker
+          label="填充"
+          value={nodeConfig.fill}
+          onChange={(value: string) => {
+            onNodeConfigChange('fill', value);
+          }}
+        />
+        <ColorPicker
+          label="边框"
+          value={nodeConfig.stroke}
+          onChange={(value: string) => {
+            onNodeConfigChange('stroke', value);
+          }}
+        />
+        <InputNumberFiled
+          label="消息数量"
+          value={nodeConfig.count}
+          onChange={value => {
+            onNodeConfigChange('count', value);
+          }}
+        />
+        <div className={`${PREFIX}-node-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            value={nodeConfig.fontSize}
+            width={68}
+            onChange={value => {
+              onNodeConfigChange('fontSize', value);
+            }}
+          />
+          <ColorPicker
+            value={nodeConfig.fontFill}
+            onChange={(value: string) => {
+              onNodeConfigChange('fontFill', value);
+            }}
+          />
+        </div>
+        {nodeConfig.type != TYPE.AUDIT && (
+          <div className="group">
+            <label>{typeOption.find(item => item.value == nodeConfig.type)?.label || '-'}</label>
+            <TreeSelect
+              showSearch
+              multiple
+              allowClear
+              value={nodeConfig.initiator?.map(item => item.origin)}
+              style={{ width: '80%' }}
+              placeholder="请选择"
+              treeData={depUserTree}
+              onChange={values => {
+                handleTreeChange(values);
+              }}
+            />
+          </div>
+        )}
+
+        {nodeConfig.type == TYPE.AUDIT && (
+          <div className={`${PREFIX}-panel-group`}>
+            <h5>审批人</h5>
+            <Radio.Group
+              onChange={(e: RadioChangeEvent) => {
+                console.log(e.target.value);
+                setIsRole(e.target.value);
+                onNodeConfigChange('audits', []);
+              }}
+              value={isRole}
+            >
+              <Radio value={1}>发起人自选</Radio>
+              <Radio value={0}>指定人</Radio>
+            </Radio.Group>
+            <div className="group">
+              {isRole ? (
+                <>
+                  <label>审批角色</label>
+                  <Select
+                    showSearch
+                    style={{ width: '100%' }}
+                    value={nodeConfig.audits?.map(item => item.value)}
+                    filterOption={(input, option) => option.props.children.indexOf(input) >= 0}
+                    onChange={value => {
+                      onNodeConfigChange('audits', [{ type: IDTYPE.ROLE, value: Number(value) }]);
+                    }}
+                  >
+                    {roleList &&
+                      roleList.map(item => (
+                        <Option key={item.ID} value={item.ID}>
+                          {item.Name}
+                        </Option>
+                      ))}
+                  </Select>
+                </>
+              ) : (
+                <>
+                  <label>审批人</label>
+                  <Select
+                    showSearch
+                    style={{ width: '100%' }}
+                    value={nodeConfig.audits?.map(item => item.value)}
+                    onChange={value => {
+                      onNodeConfigChange('audits', [{ type: IDTYPE.USER, value: Number(value) }]);
+                    }}
+                    filterOption={(input, option) => option.props.children.indexOf(input) >= 0}
+                  >
+                    {(userList || []).map(item => (
+                      <Option key={item.ID} value={item.ID}>
+                        {item.CName}
+                      </Option>
+                    ))}
+                  </Select>
+                </>
+              )}
+            </div>
+          </div>
+        )}
+      </div>
+
+      <Button style={{ marginTop: 20 }} type="primary" onClick={onSave}>
+        保存
+      </Button>
+    </div>
+  );
+};
+
+function RecthServe(props: any) {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => <Component {...props} plugin={plugin} config={config} />}
+    </FlowchartFormWrapper>
+  );
+}
+
+export default connect(({ xflow, flow, user }) => ({
+  auditList: xflow.auditList,
+  depUserTree: user.depUserTree,
+  roleList: user.roleList,
+  userList: user.list,
+}))(RecthServe);

+ 113 - 0
src/components/Flow/node/judgeNode/index.tsx

@@ -0,0 +1,113 @@
+import React, { useEffect, useMemo } from 'react';
+judgeServe;
+import judgeServe, { ComponentName, FormItem } from './mapServe';
+import { JudgeType, JudgeOptions, SiginOptions } from '../../components/judgeComponent';
+import { connect } from 'dva';
+export { judgeServe };
+
+const JudgeRect = props => {
+  const { size = { width: 130, height: 50 }, data, depUserTree } = props;
+  const { width, height } = size;
+  const { label, stroke, fill, fontFill, fontSize, type, priority, formItems } = data;
+
+  const contentText = useMemo(() => {
+    let text = [];
+    const getName = (id, data) => {
+      let name = '';
+      for (let i = 0; i < data.length; i++) {
+        let item = data[i];
+        if (item.ID == id) {
+          return item.title;
+        } else if (item.children?.length > 0) {
+          let title = getName(id, item.children);
+          if (title) return title;
+        }
+      }
+      return name;
+    };
+
+    if (formItems) {
+      let data: FormItem[] = JSON.parse(formItems);
+      data.forEach((item: FormItem) => {
+        let judge: JudgeType = item.judge;
+        const label: String = item.props.label;
+        switch (judge?.type) {
+          case ComponentName.Inner:
+            const list = judge?.values
+              .map(item => {
+                return getName(item.value, depUserTree);
+              })
+              .filter(item => item);
+            text.push('发起人属于:' + list.join('或'));
+            break;
+          case ComponentName.Number:
+            const type: Number = judge.values[0];
+            const condition = judge.condition;
+            if (!condition) break;
+            if (type != 6) {
+              let JudgeLabel = JudgeOptions.find(item => item.value == type)?.label;
+
+              text.push(`${label} ${JudgeLabel} ${condition.smallValue}`);
+            } else {
+              const { smallSign, smallValue, bigSign, bigValue } = condition;
+              if (!smallSign || !smallValue || !bigSign || !bigValue) break;
+              const getSigin = (sigin: Number) =>
+                SiginOptions.find(item => item.value == sigin)?.label;
+
+              text.push(
+                `${smallValue} ${getSigin(smallSign)} ${label} ${getSigin(bigSign)} ${bigValue} `
+              );
+            }
+            break;
+          case ComponentName.Select:
+          case ComponentName.MultiSelect:
+            const values = judge.values;
+            text.push(`${label} ${values.join(' 且 ')}`);
+            break;
+        }
+      });
+    }
+    if (text.length <= 0) text.push('其他条件进入此流程');
+    return text;
+  }, [formItems, depUserTree]);
+
+  return (
+    <div
+      style={{
+        width,
+        height,
+        padding: '6px',
+        boxShadow: '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19)',
+      }}
+    >
+      <span style={{ color: '#7E8185', float: 'right', fontSize: '8px' }}>优先级{priority}</span>
+      <div style={{ color: '#15BC83' }}>{label}</div>
+      <div
+        style={{
+          height: `${height - 32}px`,
+          overflow: 'hidden',
+          textOverflow: 'ellipsis',
+          fontSize,
+        }}
+      >
+        {(contentText || []).map((text, index) => (
+          <>
+            {index != 0 && (
+              <>
+                <br />
+                并且
+                <br />
+              </>
+            )}
+
+            {text}
+          </>
+        ))}
+      </div>
+    </div>
+  );
+};
+
+export default connect(({ user }) => ({
+  depUserTree: user.depUserTree,
+}))(JudgeRect);

+ 268 - 0
src/components/Flow/node/judgeNode/mapServe.tsx

@@ -0,0 +1,268 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { FlowchartFormWrapper } from '@antv/xflow';
+import { Position, Size, ColorPicker, InputNumberFiled, InputFiled, SelectField } from '../fields';
+import { PREFIX } from '../constants';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+import { Button } from 'antd';
+import AddCondition from '../../components/judgeModal';
+import RenderJudge, { JudgeType } from '../../components/judgeComponent';
+import { TYPE } from '../auditNode/mapServe';
+
+// export const enum TYPE {
+//   AUDIT,
+//   INITIATOR,
+//   COPYMAN,
+// }
+
+export const enum ComponentName {
+  Inner = 'InnerContactField',
+  Depart = 'DepartmentField',
+  Select = 'DDSelectField',
+  Money = 'MoneyField',
+  MultiSelect = 'DDMultiSelectField',
+  Number = 'NumberField',
+}
+
+export interface IConfig {
+  label?: string;
+  x?: number;
+  y?: number;
+  width?: number;
+  height?: number;
+  count?: number;
+  fontSize?: number;
+  fontFill?: string;
+  fill?: string;
+  stroke?: string;
+  flow_id?: string;
+  flow_node_id?: string;
+  process_code?: string;
+  type: TYPE;
+  formItems: string;
+  priority?: number; //优先级
+}
+
+export interface FormItem {
+  componentName: string;
+  props: {
+    label: string;
+    id: string;
+    options?: string[];
+    required: boolean;
+    placeholder?: string;
+  };
+  judge?: JudgeType;
+}
+
+const Component = (props: any) => {
+  const { config, plugin = {}, formItems } = props;
+  const { updateNode } = plugin;
+
+  // const formData: FormItem[] = [
+  //   {
+  //     componentName: 'InnerContactField',
+  //     props: {
+  //       label: '申请人',
+  //       id: 'InnerContactField-JEQRQ5VH',
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DepartmentField',
+  //     props: {
+  //       label: '选择部门',
+  //       id: 'DepartmentField_1VALX0GTRNVK0',
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DDSelectField',
+  //     props: {
+  //       id: 'DDSelectField_LWFCJRK508W0',
+  //       label: '文件类型',
+  //       options: [
+  //         '{"value":"新增|FT008","key":"option_1IXTB1VDV9EO0"}',
+  //         '{"value":"补充|FT009","key":"option_1DVWNV9BNZNK0"}',
+  //       ],
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'DDSelectField',
+  //     props: {
+  //       id: 'DDSelectField_21WUQ0OWR7R40',
+  //       label: '是否外带',
+  //       options: [
+  //         '{"value":"是","key":"option_DUSP6XANFKO0"}',
+  //         '{"value":"否","key":"option_CC5VQ63ZLJK0"}',
+  //       ],
+  //       required: true,
+  //     },
+  //   },
+  //   {
+  //     componentName: 'MoneyField',
+  //     props: {
+  //       id: 'MoneyField-JSCUC2GG',
+  //       label: '文件金额(元)',
+  //       placeholder: '请输入金额',
+  //       required: true,
+  //     },
+  //   },
+  // ];
+
+  const [nodeConfig, setNodeConfig] = useState<IConfig>({
+    ...config,
+  });
+  const onNodeConfigChange = (key: string, value: number | string | object) => {
+    if (key) {
+      setNodeConfig({
+        ...nodeConfig,
+        [key]: value,
+      });
+      updateNode({
+        type: TYPE.JUDGE,
+        [key]: value,
+      });
+    } else if (value instanceof Object) {
+      setNodeConfig({
+        ...nodeConfig,
+        ...value,
+      });
+      updateNode({
+        type: TYPE.JUDGE,
+        ...value,
+      });
+    }
+  };
+
+  const onSave = () => {
+    UnityAction.emit('NODE_SAVE', nodeConfig);
+  };
+
+  useEffect(() => {
+    setNodeConfig({
+      ...config,
+    });
+  }, [config]);
+
+  return (
+    <div className={`${PREFIX}-panel-body`}>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>内容</h5>
+        <InputFiled
+          label="标题"
+          value={nodeConfig.label}
+          onChange={value => {
+            onNodeConfigChange('label', value);
+          }}
+        />
+      </div>
+      <div className={`${PREFIX}-panel-group`}>
+        <h5>样式</h5>
+        <Position
+          x={nodeConfig.x}
+          y={nodeConfig.y}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <Size
+          width={nodeConfig.width}
+          height={nodeConfig.height}
+          onChange={(key, value) => {
+            onNodeConfigChange(key, value);
+          }}
+        />
+        <ColorPicker
+          label="填充"
+          value={nodeConfig.fill}
+          onChange={(value: string) => {
+            onNodeConfigChange('fill', value);
+          }}
+        />
+        <ColorPicker
+          label="边框"
+          value={nodeConfig.stroke}
+          onChange={(value: string) => {
+            onNodeConfigChange('stroke', value);
+          }}
+        />
+        <InputNumberFiled
+          label="消息数量"
+          value={nodeConfig.count}
+          onChange={value => {
+            onNodeConfigChange('count', value);
+          }}
+        />
+        <div className={`${PREFIX}-node-text-style`}>
+          <InputNumberFiled
+            label="字号"
+            value={nodeConfig.fontSize}
+            width={68}
+            onChange={value => {
+              onNodeConfigChange('fontSize', value);
+            }}
+          />
+          <ColorPicker
+            value={nodeConfig.fontFill}
+            onChange={(value: string) => {
+              onNodeConfigChange('fontFill', value);
+            }}
+          />
+        </div>
+        <InputNumberFiled
+          label="优先级"
+          value={nodeConfig.priority}
+          width={68}
+          onChange={value => {
+            onNodeConfigChange('priority', value);
+          }}
+        />
+        <AddCondition
+          items={formItems}
+          formItems={nodeConfig.formItems}
+          onOk={(values: FormItem[]) => {
+            console.log('===formItems===', values);
+            let newFormItems = [];
+            let oldFormItems = JSON.parse(nodeConfig.formItems || '[]');
+            values.forEach(item => {
+              let id = item.props.id;
+              // 判断是否含有旧的item
+              let oldItem = oldFormItems.find(formItem => formItem.props.id == id);
+              if (oldItem) {
+                newFormItems.push(oldItem);
+              } else {
+                newFormItems.push(item);
+              }
+            });
+            onNodeConfigChange('formItems', JSON.stringify(newFormItems));
+          }}
+        />
+        <RenderJudge
+          formItems={nodeConfig.formItems}
+          onChange={(values: FormItem[]) => {
+            console.log('===formItems===', values);
+            onNodeConfigChange('formItems', JSON.stringify(values));
+          }}
+        />
+      </div>
+
+      <Button style={{ marginTop: 20 }} type="primary" onClick={onSave}>
+        保存
+      </Button>
+    </div>
+  );
+};
+
+function RecthServe(props: any) {
+  return (
+    <FlowchartFormWrapper {...props}>
+      {(config, plugin) => <Component {...props} plugin={plugin} config={config} />}
+    </FlowchartFormWrapper>
+  );
+}
+
+export default connect(({ xflow }) => ({ auditList: xflow.auditList, formItems: xflow.formData }))(
+  RecthServe
+);

+ 624 - 0
src/models/dataMeterNew.js

@@ -0,0 +1,624 @@
+import {
+  getProject,
+  getFaultAnalysis,
+  getIssueList,
+  getProjectActive,
+  getNotificationList,
+  getMediaList,
+  getMonitorList,
+  getUserGuide,
+  getDailyList,
+  getProjectProgress,
+  deleteFile,
+  getProjectTotalProgress,
+  getLayoutOptions,
+  saveLayoutOptions,
+  getProjectAlarm,
+  getBreakdownRecord,
+  getProjectList,
+  getAutoPatrol,
+  getBreakdownList,
+  getPatrolRecord,
+} from '@/services/DataMeter';
+import { queryProjectFileList } from '@/services/FileAdmin';
+import { getprojectPlanProgress, getProjectRealProgress } from '@/services/projectPlanProgress';
+import { getRepairRecord } from '@/services/EquipmentMaintenance';
+import { queryConfigList, queryTemplateList } from '@/services/CharUtils';
+import { queryPatrolRecord } from '@/services/patrol';
+import { message } from 'antd';
+import { DEFAULT_LAYOUT, DEFAULT_MAP_LAYOUT } from '@/components/DataMeter/config';
+import { getDefaultConfList } from '@/services/ProjectAdmin';
+const PLAN_TYPE = {
+  15: 1,
+  3: 3,
+  12: 4,
+};
+
+export default {
+  namespace: 'dataMeterNew',
+  state: {
+    projectDetail: {},
+    faultAnalysis: [],
+    issueList: [],
+    projectActive: [],
+    news: [],
+    memorabilia: [],
+    awards: [],
+    notificationList: [],
+    projectAlarmList: [],
+    repairList: [],
+    breakdownList: [],
+    mediaImgList: [],
+    mediaVideo: null,
+    monitorList: [],
+    userGuideList: [],
+    dailyList: [],
+    projectProgress: null,
+    projectTotalProgress: {
+      AllProgress: 0,
+    },
+    layout: {
+      0: {
+        config_json: [],
+      },
+      1: {
+        config_json: [],
+      },
+      2: {
+        config_json: [],
+      },
+    },
+    chartConfigList: [],
+    realProgress: {},
+    progress: {},
+    // currentChart: {}
+    projectList: [],
+    autoReport: {},
+    patrolRecordDetail: {},
+    patrolList: [],
+    fileList: [],
+    defaultConf: {},
+    layoutAdmin: {
+      0: {
+        config_json: [],
+      },
+      1: {
+        config_json: [],
+      },
+      2: {
+        config_json: [],
+      },
+    },
+  },
+
+  effects: {
+    *getAutoPatrol({ payload, callback }, { call, put }) {
+      const response = yield call(getAutoPatrol, payload);
+      if (response) {
+        // 查询数据报表详情
+        const { data } = yield call(queryPatrolRecord, { recordId: response.data[0].Id });
+        if (data) {
+          const creatorName = data.CreatorUser && data.CreatorUser.CName;
+          var status = {};
+          let Items = [];
+          const getItems = (item, patrolType) => {
+            let key;
+            if (patrolType == 1) {
+              key = item.PatrolName;
+            } else {
+              key = item.DeviceCode + '-' + item.DeviceName;
+            }
+            item.patrolType = patrolType;
+            item.ThresholdEnum = item.TemplateItem.ThresholdEnum;
+            item.Type = item.TemplateItem.Type;
+            if (!status[key]) status[key] = { normal: 0, error: 0 };
+            if (item.Status == 1) {
+              status[key].error++;
+            } else {
+              status[key].normal++;
+            }
+            return item;
+          };
+          data.ItemsExtend.forEach(item => {
+            console.log(Items);
+            if (item.PatrolCardRecordItemAssocThreshold) {
+              var arr = item.PatrolCardRecordItemAssocThreshold.map(i => getItems(i, 1));
+              Items = Items.concat(arr);
+            } else {
+              Items.push(getItems(item, 0));
+            }
+          });
+          data.Items = Items;
+
+          data.Points.forEach(item => {
+            let key;
+            if (item.PatrolType == 1) {
+              key = item.DeviceName;
+            } else {
+              key = item.DeviceCode + '-' + item.DeviceName;
+            }
+            item.creatorName = creatorName;
+            item.level = `${item.ExceptionLevel || '-'}/${item.DeviceLevel || '-'}`;
+            item.status = status[key] || { normal: 0, error: 0 };
+          });
+          console.log(data);
+          callback && callback(data);
+          yield put({
+            type: 'save',
+            payload: { autoReport: data },
+          });
+        }
+      }
+    },
+    *getProject({ payload }, { call, put }) {
+      const response = yield call(getProject, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectDetail: response.data },
+        });
+      }
+    },
+    *getFaultAnalysis({ payload }, { call, put }) {
+      const response = yield call(getFaultAnalysis, payload);
+      if (response) {
+        var list = [];
+        response.data.forEach(item => {
+          item.Details.forEach(err => {
+            list.push({
+              ...item,
+              ...err,
+            });
+          });
+        });
+        yield put({
+          type: 'save',
+          payload: { faultAnalysis: list },
+        });
+      }
+    },
+    *getIssueList({ payload }, { call, put }) {
+      const response = yield call(getIssueList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { issueList: response.data.list },
+        });
+      }
+    },
+    *getRepairRecord({ payload }, { call, put }) {
+      const response = yield call(getRepairRecord, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { repairList: response.data.list },
+        });
+      }
+    },
+    *getBreakdownRecord({ payload }, { call, put }) {
+      const response = yield call(getBreakdownList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { breakdownList: response.data.list },
+        });
+      }
+    },
+    *getPatrolRecord({ payload }, { call, put }) {
+      const response = yield call(getPatrolRecord, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { patrolList: response.data.list },
+        });
+      }
+    },
+    *getProjectAlarm({ payload }, { call, put }) {
+      const response = yield call(getProjectAlarm, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectAlarmList: response.data.list },
+        });
+      }
+    },
+    *getProjectActive({ payload }, { call, put }) {
+      const response = yield call(getProjectActive, payload);
+      if (response) {
+        let stateName;
+
+        // 0-项目动态 1-新闻动态 2-大事记 3-所获奖项
+        switch (payload.type) {
+          case 0:
+            stateName = 'projectActive';
+            break;
+          case 1:
+            stateName = 'news';
+            break;
+          case 2:
+            stateName = 'memorabilia';
+            break;
+          case 3:
+            stateName = 'awards';
+            break;
+        }
+        if (!stateName) return;
+        yield put({
+          type: 'save',
+          payload: { [stateName]: response.data.list },
+        });
+      }
+    },
+    *getNotificationList({ payload }, { call, put }) {
+      const response = yield call(getNotificationList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { notificationList: response.data.list },
+        });
+      }
+    },
+    *getMediaList({ payload }, { call, put }) {
+      const response = yield call(getMediaList, payload);
+      if (response) {
+        var mediaVideo = null,
+          mediaImgList = [];
+        response.data.forEach(item => {
+          if (/\.mp4$/.test(item.Name)) {
+            if (!mediaVideo) {
+              mediaVideo = item;
+            }
+          } else {
+            mediaImgList.push(item);
+          }
+        });
+        yield put({
+          type: 'save',
+          payload: {
+            mediaVideo,
+            mediaImgList,
+          },
+        });
+      }
+    },
+    *getMonitorList({ payload }, { call, put }) {
+      const response = yield call(getMonitorList, payload);
+      if (response) {
+        var obj = JSON.parse(response.data.config_json) || {};
+        yield put({
+          type: 'save',
+          // payload: { monitorList: response.data.list },
+          payload: { monitorList: Object.keys(obj).map(key => obj[key]) || [] },
+        });
+      }
+    },
+    *getUserGuide({ payload }, { call, put }) {
+      const response = yield call(getUserGuide, payload);
+      if (response) {
+        var list = [];
+        response.data.list.forEach(item => {
+          item.UserGuideFiles.forEach(file => {
+            list.push({
+              ...item,
+              ...file,
+            });
+          });
+        });
+        yield put({
+          type: 'save',
+          payload: { userGuideList: list },
+        });
+      }
+    },
+    *getDailyList({ payload }, { call, put }) {
+      const response = yield call(getDailyList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { dailyList: response.data.list },
+        });
+      }
+    },
+
+    *getProjectPlanProgress({ payload, callback }, { call, put }) {
+      const response = yield call(getprojectPlanProgress, payload);
+      if (response) {
+        var progress = response.data;
+        Object.keys(progress).forEach(k => {
+          if (
+            progress[k] == '0001-01-01T00:00:00Z' ||
+            progress[k] == '0001-01-01 00:00:00' ||
+            progress[k] == '0001-01-01 00:00:00 +0000 UTC'
+          ) {
+            progress[k] = '';
+          }
+        });
+        yield put({
+          type: 'save',
+          payload: { progress },
+        });
+      }
+    },
+    *getProjectRealProgress({ payload }, { call, put, select }) {
+      const calcProgress = data => {
+        let count = 0,
+          total = 0;
+        data.forEach(item => {
+          total += item.TotalCount;
+          count += item.RealCount;
+        });
+        return ((count / total) * 100).toFixed(2);
+      };
+      let { realProgress } = yield select(s => s.dataMeterNew);
+      console.log(realProgress);
+      var keys = Object.keys(PLAN_TYPE);
+      for (let i = 0; i < keys.length; i++) {
+        let k = keys[i];
+        var response = yield call(getProjectRealProgress, {
+          ...payload,
+          type: PLAN_TYPE[k],
+        });
+        if (response) {
+          let tempRealProgress = response.data.Task.map(item => ({
+            ...item,
+            RealCount: item.FinishList.length || 0,
+            TotalCount: item.DeviceCodes ? item.DeviceCodes.length : 0,
+            children: null,
+          }));
+          console.log(tempRealProgress);
+          realProgress[k] = calcProgress(tempRealProgress);
+        }
+      }
+      yield put({
+        type: 'save',
+        payload: { realProgress: { ...realProgress } },
+      });
+    },
+    *getProjectProgress({ payload }, { call, put }) {
+      const response = yield call(getProjectProgress, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectProgress: response.data },
+        });
+      }
+    },
+    *deleteFile({ payload }, { call, put }) {
+      const response = yield call(deleteFile, payload);
+      if (response) {
+        yield put({
+          type: 'getMediaList',
+          payload: payload,
+        });
+      }
+    },
+    *getProjectTotalProgress({ payload }, { call, put }) {
+      const response = yield call(getProjectTotalProgress, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { projectTotalProgress: response.data },
+        });
+      }
+    },
+    *getLayoutOptions({ payload }, { call, put, select }) {
+      // if (payload.is_default == 1) {
+      //   // 默认配置的projectId为0
+      //   payload.projectId = 0;
+      // }
+      const response = yield call(getLayoutOptions, payload);
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        if (payload.is_default == 1) {
+          layout = yield select(state => state.dataMeterNew.layoutAdmin);
+        }
+        var defaultLayout = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+        var data = response.data;
+        try {
+          data.config_json = JSON.parse(data.config_json);
+          // response.data.config_json = defaultLayout;
+        } catch (error) {
+          // 解析失败则使用默认布局
+          data.config_json = defaultLayout;
+          console.log(error);
+        }
+        layout[payload.sub_module] = data;
+        if (payload.is_default == 0)
+          yield put({
+            type: 'save',
+            payload: { layout: { ...layout } },
+          });
+        else
+          yield put({
+            type: 'save',
+            payload: { layoutAdmin: { ...layout } },
+          });
+      }
+    },
+    *saveLayoutOptions({ payload }, { call, put, select }) {
+      // if (payload.is_default == 1) {
+      //   // 默认配置的projectId为0
+      //   payload.projectId = 0;
+      // }
+      if (payload.config_json == '[]') {
+        message.error('保存的配置不能为空');
+        return;
+      }
+      var response = yield call(saveLayoutOptions, payload);
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        if (payload.is_default == 1) {
+          layout = yield select(state => state.dataMeterNew.layoutAdmin);
+        }
+        console.log(payload.config_json);
+        layout[payload.sub_module].config_json = JSON.parse(payload.config_json);
+        if (payload.is_default == 0)
+          yield put({
+            type: 'save',
+            payload: { layout: { ...layout } },
+          });
+        else
+          yield put({
+            type: 'save',
+            payload: { layoutAdmin: { ...layout } },
+          });
+      }
+    },
+    *resizeLayout({ payload }, { call, select, put }) {
+      // 默认配置的projectId为0
+      const response = yield call(getLayoutOptions, {
+        ...payload,
+        // projectId: 0,
+      });
+      if (response) {
+        var layout = yield select(state => state.dataMeterNew.layout);
+        var defaultLayout = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+        var config_json = response.data.config_json
+          ? JSON.parse(response.data.config_json)
+          : defaultLayout;
+        // response.data.config_json = defaultLayout;
+
+        // 将默认配置保存到本地
+        layout[payload.sub_module].config_json = config_json;
+        yield put({
+          type: 'save',
+          payload: { layout: { ...layout } },
+        });
+        yield call(saveLayoutOptions, {
+          ...payload,
+          config_json: JSON.stringify(config_json),
+          is_default: 0,
+        });
+      }
+    },
+    *resizeDefaultLayout({ payload }, { call, select, put }) {
+      var layout = yield select(state => state.dataMeterNew.layout);
+      var config_json = payload.sub_module === 0 ? DEFAULT_MAP_LAYOUT : DEFAULT_LAYOUT;
+
+      // 将默认配置保存到本地
+      layout[payload.sub_module].config_json = config_json;
+      yield put({
+        type: 'save',
+        payload: { layout: { ...layout } },
+      });
+      yield call(saveLayoutOptions, {
+        ...payload,
+        config_json: JSON.stringify(config_json),
+        is_default: 0,
+      });
+    },
+    *queryConfigList({ payload, callback }, { call, put }) {
+      const {
+        data: { list: tplList },
+      } = yield call(queryTemplateList, { pageSize: 999 });
+      const {
+        data: { list: configList },
+      } = yield call(queryConfigList, {
+        ...payload,
+        isNew: 1,
+        pageSize: 9999,
+      });
+      let list = [];
+      configList.map(item => {
+        let template = tplList.find(tpl => tpl.ID == item.type);
+        item.options = JSON.parse(item.options);
+        item.template = template;
+        list.push(item);
+      });
+      callback && callback(list);
+      yield put({
+        type: 'save',
+        payload: {
+          chartConfigList: list,
+        },
+      });
+    },
+    *queryConfig({ payload, callback }, { call, put }) {
+      const response = yield call(queryConfigList, payload);
+      if (response) {
+        var currentChart = response.data.list[0];
+        currentChart.options = JSON.parse(currentChart.options);
+        let { options, configs, type } = currentChart;
+
+        // 将接口返回的实时数据更新到图表中
+        if (type != 'pfd') {
+          if (!options.series) {
+            callback && callback(currentChart);
+          }
+          let series = options.series[0];
+          if (type == 'pie') {
+            configs.forEach((item, index) => {
+              series.data[index].value = item.value;
+            });
+          } else if (series.type == 'bar') {
+            configs.forEach((item, index) => {
+              series.data[index] = item.value;
+            });
+          }
+        }
+        callback && callback(currentChart);
+        // yield put({
+        //   type: 'save',
+        //   payload: {
+        //     currentChart,
+        //   },
+        // });
+      }
+    },
+    *getProjectList({ payload, callback }, { call, put }) {
+      const response = yield call(getProjectList, payload);
+      if (response) {
+        let nowDate = new Date();
+        (response.data.list || []).map(item => {
+          let type;
+          if (!item.EndDate) {
+            type = 2;
+          } else {
+            type = nowDate >= new Date(item.EndDate) ? 2 : 1;
+          }
+          item.type = type;
+        });
+        yield put({
+          type: 'save',
+          payload: { projectList: response.data.list },
+        });
+        console.log(response.data.list);
+        callback && callback(response.data.list);
+      }
+    },
+    *queryProjectFileList({ payload }, { call, put, select }) {
+      try {
+        const response = yield call(queryProjectFileList, {
+          ...payload,
+          deviceCode: -1,
+        });
+        if (response) {
+          yield put({
+            type: 'save',
+            payload: { fileList: response.data },
+          });
+        }
+      } catch (e) {
+        console.error(e);
+      }
+    },
+    *getDefaultConfList({ payload }, { call, put }) {
+      const response = yield call(getDefaultConfList, payload);
+      if (response) {
+        yield put({
+          type: 'save',
+          payload: { defaultConf: response.data },
+        });
+      }
+    },
+  },
+
+  reducers: {
+    save(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+};

+ 64 - 0
src/pages/DDLogin/index.js

@@ -0,0 +1,64 @@
+import React, { useEffect, useState } from 'react';
+import * as dd from 'dingtalk-jsapi';
+import { message } from 'antd';
+import { LoadingOutlined, CheckOutlined } from '@ant-design/icons';
+import { bindDDCode } from '@/services/boom';
+
+function DDLogin(props) {
+  const {
+    match: {
+      params: { dingUserId },
+    },
+  } = props;
+  const [info, setInfo] = useState('');
+  useEffect(() => {
+    if (dd.env.platform != 'notInDingTalk') {
+      dd.ready(function() {
+        dd.runtime.permission.requestAuthCode({
+          corpId: 'ding0cdce2d5dbf986d9', // 企业id
+          onSuccess: async function(info) {
+            setTimeout(() => {
+              setInfo(info.code);
+            }, 500);
+            let res = await bindDDCode(dingUserId, info.code);
+            console.log(res);
+
+            // 通过该免登授权码可以获取用户身份
+          },
+          onFail: error => {
+            setInfo(JSON.stringify(error));
+          },
+        });
+      });
+    }
+  }, []);
+
+  return (
+    <div
+      style={{
+        width: '100vw',
+        fontSize: 24,
+        paddingTop: 50,
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        flexDirection: 'column',
+      }}
+    >
+      {info ? (
+        <>
+          <CheckOutlined style={{ fontSize: 40, color: '#52c41a' }} />
+          <div>授权成功</div>
+          {/* <div>{info}</div> */}
+        </>
+      ) : (
+        <>
+          <LoadingOutlined style={{ fontSize: 40, color: '#1890ff' }} />
+          <div>授权中...</div>
+        </>
+      )}
+    </div>
+  );
+}
+
+export default DDLogin;

+ 0 - 0
src/pages/Mobile/DataMeter/Alarm.js


+ 118 - 0
src/pages/Mobile/DataMeter/Chart.js

@@ -0,0 +1,118 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { connect } from 'dva';
+import moment from 'moment';
+import style from './Chart.less';
+import { Card, Empty, Spin } from 'antd';
+import { getOptions } from '@/components/ChartUtils/utils';
+const dataCache = {};
+let timer;
+function DataCenter(props) {
+  const [chart, setChart] = useState();
+  // const [timer, setTimer] = useState();
+  const [chartOptions, setChartOptions] = useState({});
+  const chartEle = useRef();
+  const signalRef = useRef();
+  const [loading, setLoading] = useState(false);
+  const {
+    chartConfigList,
+    dispatch,
+    location: {
+      query: { projectId },
+    },
+  } = props;
+  // const { projectId } = props.location; //?
+  // const projectId = 92;
+
+  const handleClickTabs = id => {
+    if (loading) return;
+    let item = chartConfigList.find(c => c.id == id);
+    setLoading(true);
+    setChartOptions({ ...item });
+  };
+
+  useEffect(() => {
+    if (projectId) {
+      // 获取自定义图表列表
+      dispatch({
+        type: 'dataMeterNew/queryConfigList',
+        payload: {
+          projectId,
+        },
+      });
+    }
+  }, [projectId]);
+
+  // useEffect(() => {
+  //   if (!chartOptions) return;
+  //   renderChart();
+  // }, [chartOptions]);
+
+  useEffect(() => {
+    if (chartConfigList && chartConfigList.length > 0) {
+      handleClickTabs(chartConfigList[0].id);
+    }
+  }, [chartConfigList]);
+
+  // const getOptionsForConfig = item => {
+  //   setChartOptions(item);
+  //   clearTimeout(timer);
+  //   let t = setTimeout(() => {
+  //     getOptionsForConfig({ ...item });
+  //   }, 1000 * 60);
+  //   setTimer(t);
+  // };
+
+  const renderChart = async e => {
+    var chartWindow = e.target.contentWindow;
+    let datas = chartOptions.configs;
+    let values = chartOptions.options;
+    let formula = JSON.parse(chartOptions.formula || '[]');
+    if (!datas) return;
+    try {
+      let options = await getOptions(values, datas, formula, projectId);
+      chartWindow.render(options);
+    } catch (error) {
+      console.log(error);
+    }
+    setLoading(false);
+  };
+
+  return (
+    <div className={style.body}>
+      <Card title="数据项" bodyStyle={{ padding: 0 }}>
+        <ul className={style.list}>
+          {(chartConfigList || []).map(item => (
+            <li
+              key={item.id}
+              style={{
+                border: chartOptions.id == item.id ? '1px solid #008dff' : '1px solid #fff',
+              }}
+              onClick={() => handleClickTabs(item.id)}
+            >
+              {item.name}
+            </li>
+          ))}
+        </ul>
+        {chartConfigList.length == 0 && (
+          <Empty style={{ color: '#fff', fontSize: 20, marginTop: 40 }} />
+        )}
+      </Card>
+      <Card title="图表" bodyStyle={{ paddingBottom: 0 }}>
+        <Spin spinning={loading}>
+          {chartOptions.template?.Content && (
+            <iframe
+              style={{ width: '100%', height: 'calc(50vh - 80px)', border: 'none' }}
+              // srcDoc拼上id  防止当模板一致时无法触发onLoad
+              srcDoc={chartOptions?.template?.Content + `<div id="${chartOptions.id}"></div>`}
+              onLoad={renderChart}
+            ></iframe>
+          )}
+        </Spin>
+      </Card>
+    </div>
+  );
+}
+export default connect(({ dataMeterNew, user }) => ({
+  chartConfigList: dataMeterNew.chartConfigList,
+  user: user.currentUser,
+}))(DataCenter);

+ 61 - 0
src/pages/Mobile/DataMeter/Chart.less

@@ -0,0 +1,61 @@
+.body {
+  background-color: #0d1a2b;
+  color: #fff;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  :global {
+    .ant-card {
+      background: transparent;
+      flex: 1;
+    }
+    .ant-card-head {
+      color: #fff;
+    }
+  }
+}
+
+.title {
+  font-size: 20px;
+  padding: 10px;
+}
+
+.listWrapper {
+  height: 40vh;
+  overflow-y: auto;
+}
+
+.list {
+  display: flex;
+  justify-content: space-evenly;
+  flex-wrap: wrap;
+  height: 100%;
+  max-height: calc(50vh - 74px);
+  overflow-y: auto;
+  padding: 10px;
+  margin: 0;
+  li {
+    width: 100%;
+    padding: 6px;
+    text-align: center;
+    color: #fff;
+    font-size: 14px;
+    margin-bottom: 10px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+.listItem {
+  width: 80%;
+  text-align: center;
+  border: 1px solid #fff;
+  border-radius: 5px;
+  margin: 5px 0px;
+  padding: 5px 0px;
+}
+
+.chartWrapper {
+  height: 40vh;
+}

+ 39 - 0
src/pages/Mobile/DataMeter/Craft.js

@@ -0,0 +1,39 @@
+import React from 'react';
+import Flow, { FLOW_TYPE } from '@/components/Flow';
+import { connect } from 'dva';
+import { UnityAction } from '@/utils/utils';
+
+const graphData = JSON.parse(localStorage.graphData || null);
+
+// @connect(({ xflow }) => ({ flowDetail: xflow.flowDetail }))
+class Craft extends React.PureComponent {
+  state = {
+    flowDetail: graphData || { nodes: [], edges: [] },
+  };
+  componentDidMount() {
+    const {
+      dispatch,
+      match: {
+        params: { flowId },
+      },
+    } = this.props;
+
+    UnityAction.on('NODE_SAVE', nodeConfig => {
+      console.log(nodeConfig);
+    });
+  }
+  componentWillUnmount() {
+    UnityAction.off('NODE_SAVE');
+  }
+  render() {
+    const { flowDetail } = this.state;
+
+    return (
+      <div>
+        <Flow meta={{ type: 'edit', flowId: 1 }} flowDetail={flowDetail} />
+      </div>
+    );
+  }
+}
+
+export default Craft;

+ 0 - 0
src/pages/Mobile/DataMeter/Report.js


+ 16 - 0
src/pages/PurchaseAdmin/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;
+}

+ 100 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/ApprovalProcess.tsx

@@ -0,0 +1,100 @@
+import React, { useState } from 'react';
+
+import { queryUserListByRoleID } from '@/services/boom';
+import { connect } from 'dva';
+import { PlusOutlined } from '@ant-design/icons';
+import { Popover, Radio, RadioChangeEvent, Spin, Steps } from 'antd';
+
+const { Step } = Steps;
+
+enum TYPR {
+  ROLE = 'role',
+  USER = 'user',
+}
+
+const ApprovalProcess = props => {
+  const { id, approvalProcess, userList, onChange } = props;
+  const [selectUserList, setSelectUserList] = useState([]);
+  const [curNodeIdx, setCurNodeIdx] = useState(-1);
+  const [loading, setLoading] = useState(false);
+  // console.log(approvalProcess);
+
+  const list = approvalProcess[id];
+  list?.forEach(item => {
+    if (item.length > 1 && item[0].type == TYPR.USER) {
+      item.forEach(curUser => {
+        curUser.name = userList.find(user => user.ID == curUser.value)?.CName || '-';
+      });
+    } else if (item.length == 1 && item[0].type == TYPR.USER) {
+      item[0].name = userList.find(user => user.ID == item[0].value)?.CName || '-';
+    } else if (item.length == 1 && item[0].nowType == TYPR.USER) {
+      item[0].name = userList.find(user => user.ID == item[0].nowValue)?.CName || '-';
+    } else {
+      item[0].name = null;
+    }
+  });
+  console.log(list);
+
+  const onStepsChange = async (current, list) => {
+    setLoading(true);
+    const itemNode = list[current][0];
+    if (itemNode.type !== 'role') return;
+    const data = await queryUserListByRoleID({ role_id: itemNode.value });
+    setCurNodeIdx(current);
+    setSelectUserList(data);
+    setLoading(false);
+  };
+
+  const selectedUserId = ({ target: { value } }: RadioChangeEvent) => {
+    //userId
+    const name = userList.find(user => user.ID == value)?.CName || '-';
+    const data = { nowType: TYPR.USER, nowValue: Number(value), name }; //type: TYPR.USER, value: Number(value)
+    list[curNodeIdx][0] = { ...list[curNodeIdx][0], ...data };
+    console.log(list);
+    onChange?.({ ...approvalProcess, [id]: list });
+  };
+
+  const content = (
+    <Spin spinning={loading}>
+      <Radio.Group onChange={selectedUserId}>
+        {selectUserList.map(item => (
+          // <Button onClick={() => selectedUserId(item.user_id)}>{item.c_name}</Button>
+          <Radio.Button value={item.user_id}>{item.c_name}</Radio.Button>
+        ))}
+      </Radio.Group>
+    </Spin>
+  );
+
+  return (
+    <>
+      <Steps current={-1} direction="vertical" onChange={value => onStepsChange(value, list)}>
+        {list?.map(item => (
+          <Step
+            key={item[0]?.value}
+            icon={
+              item[0].type != TYPR.USER || item[0].nowType == TYPR.USER ? (
+                <Popover
+                  placement="bottomLeft"
+                  title={'选择审批人'}
+                  content={content}
+                  trigger="click"
+                  overlayStyle={{ width: '300px' }}
+                >
+                  <PlusOutlined />
+                </Popover>
+              ) : null
+              // <PlusOutlined />
+            }
+            title={item[0]?.name}
+          />
+        ))}
+      </Steps>
+      {/* {selectUserList.map(item => (
+        <Button onClick={() => selectedUserId(item.user_id)}>{item.c_name}</Button>
+      ))} */}
+    </>
+  );
+};
+export default connect(({ xflow, detail, user }) => ({
+  userList: user.list,
+}))(ApprovalProcess);

+ 130 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/AuditDetailed.js

@@ -0,0 +1,130 @@
+import DDComponents from '@/components/DDComponents';
+import React, { useMemo, useState } from 'react';
+import { Form } from '@ant-design/compatible';
+import '@ant-design/compatible/assets/index.css';
+
+const layout = {
+  labelCol: {
+    span: 8,
+  },
+  wrapperCol: {
+    span: 16,
+  },
+};
+
+const AuditDetailed = props => {
+  // const [form] = Form.useForm();
+  const { items, form } = props;
+
+  const behavior = useMemo(() => {
+    let data = {};
+    items.forEach(d => {
+      const item = d.props;
+      if (item.behaviorLinkage) {
+        const key = item.id;
+        const options = item.options.map(o => {
+          let data;
+          try {
+            data = JSON.parse(o);
+          } catch (error) {
+            data = { key: o, value: o };
+          }
+          return data;
+        });
+        item.behaviorLinkage.forEach(b => {
+          const value = b.value;
+          b.targets.forEach(t => {
+            data[t.fieldId] = { key, value: options.find(o => o.key == value)?.value };
+          });
+        });
+      }
+    });
+
+    return data;
+  }, [items]);
+
+  const onFinish = values => {
+    console.log(values);
+  };
+
+  const GetComponent = item => {
+    const {
+      id,
+      label,
+      bizAlias,
+      required,
+      placeholder,
+      options,
+      align,
+      statField,
+      hideLabel,
+      objOptions,
+      format,
+      pushToAttendance,
+      labelEditableFreeze,
+      requiredEditableFreeze,
+      unit,
+      extract,
+      link,
+      payEnable,
+      bizType,
+      childFieldVisible,
+      notPrint,
+      verticalPrint,
+      hiddenInApprovalDetail,
+      disabled,
+      notUpper,
+      children, // 子控件
+    } = item.props;
+    // 判断是否属于关联项
+    if (behavior[id]) {
+      const { key, value } = behavior[id];
+      let currentValue = form.getFieldValue(key);
+      try {
+        currentValue = JSON.parse(currentValue);
+      } catch (error) {}
+      // 判断是否需要渲染
+      if (currentValue instanceof Array) {
+        if (currentValue?.indexOf(value) == -1) return null;
+      } else {
+        if (currentValue != value) return null;
+      }
+    }
+    let formLabel;
+    if (bizAlias) {
+      formLabel = bizAlias;
+    } else {
+      try {
+        // 判断label是否为JSON数组
+        formLabel = JSON.parse(label).join(',');
+      } catch (error) {
+        formLabel = label;
+      }
+    }
+
+    const component = DDComponents({ item });
+    if (!component) return null;
+    return (
+      <Form.Item label={formLabel}>
+        {typeof component == 'string'
+          ? component
+          : form.getFieldDecorator(id, {
+              rules: [{ required }],
+            })(component)}
+        {notUpper == 1 && <p>大写</p>}
+      </Form.Item>
+    );
+  };
+
+  return (
+    <Form
+      style={{ height: '400px', overflowY: 'scroll', paddingRight: 20 }}
+      layout="vertical"
+      autoComplete="off"
+    >
+      {items.map(item => GetComponent(item))}
+    </Form>
+  );
+};
+
+export default AuditDetailed;

+ 271 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/AuditFlow.js

@@ -0,0 +1,271 @@
+import React, { useMemo, useEffect, useState, useRef } from 'react';
+import { Steps, Popover, Alert, Button, Spin } from 'antd';
+import styles from './Index.less';
+import { queryDDProcessesForecast } from '@/services/boom';
+import { connect } from 'dva';
+
+const { Step } = Steps;
+
+function AuditFlow(props) {
+  const {
+    processCode,
+    deptId,
+    userId,
+    formComponentValues,
+    // activityId,
+    direction,
+    status,
+    tasks,
+    userList,
+    cc_userids = [],
+    canShowAudit = false,
+    onApprove,
+  } = props;
+  const [flow, setFlow] = useState({ workflowActivityRules: [] });
+  const [loading, setLoading] = useState(false);
+  const timerRef = useRef({
+    id: '',
+    status: false,
+  });
+  const current = useMemo(() => {
+    if (!tasks || tasks.length == 0) {
+      return false;
+    } else {
+      let current = 0;
+      flow.workflowActivityRules.forEach((item, index) => {
+        let task = tasks.find(task => task.activity_id == item.activityId);
+        if (task) {
+          if (task.task_status == 'COMPLETED') {
+            // 完成时节点为下一级
+            current = index + 1;
+          } else {
+            // 未完成则为当前节点
+            current = index;
+          }
+        }
+      });
+      return current;
+    }
+  }, [tasks, flow]);
+
+  const showAduit = useMemo(() => {
+    if (!canShowAudit) return false;
+    //当前处理中的审批人和登录人是一个人显示审批通过和拒绝
+    if (!tasks || tasks.length == 0) {
+      return false;
+    } else {
+      // 查询处于RUNNING的task activityId
+      const runningTask = tasks.filter(item => item.task_status == 'RUNNING');
+      // const task = tasks.find(task => task.activity_id == activityId);
+      if (runningTask.length == 0) return false;
+      // if (task.task_status == 'RUNNING' && task.userid == userId) return true;
+      return runningTask.find(task => task.userid == userId);
+    }
+  }, [tasks, userId]);
+
+  const getTaskId = () => {
+    const runningTask = tasks.find(item => item.task_status == 'RUNNING' && item.userid == userId);
+    if (runningTask) return runningTask.taskid;
+  };
+
+  const getDesc = item => {
+    const { activityId } = item;
+    if (!tasks || tasks.length == 0) return '';
+    if (item.activityName == '抄送人') {
+      let names = cc_userids
+        .map(userId => {
+          let user = userList.find(u => u.DingUserId == userId);
+          return user?.CName;
+        })
+        .filter(item => item);
+      return names.join(',');
+    } else {
+      const task = tasks.find(task => task.activity_id == activityId);
+      if (!task) return;
+      const user = userList.find(user => task.userid == user.DingUserId);
+      let result = null;
+      if (task.task_result !== 'NONE') {
+        result = (
+          <>
+            <br />
+            审批结果: {TASK_RESULT[task.task_result]}
+            <br />
+          </>
+        );
+      }
+      return (
+        <>
+          {user?.CName || '未知操作人'}({TASK_STATUS[task.task_status]}){result}
+        </>
+      );
+    }
+  };
+  const renderAlert = () => {
+    // const audit_comment = history.list[0]?.audit_comment;
+    let item = '';
+    switch (status) {
+      case -1:
+        item = <Alert message="已失效" type="error" />;
+        break;
+      case 0:
+        if (!flow.list || flow.list.FlowNodes?.length == 0) return;
+        item = <Alert message="审批拒绝" type="error" />;
+        break;
+      case 1:
+        item = <Alert message="等待审核中" type="info" />;
+        break;
+      case 2:
+        item = <Alert message={`审批被拒绝`} type="error" />;
+        break;
+      case 3:
+        item = <Alert message="审批通过" type="success" />;
+        break;
+      case -1:
+        item = <Alert message="已失效" type="error" />;
+        break;
+    }
+
+    return <div style={{ margin: '20px auto' }}>{item}</div>;
+  };
+  const customDot = (dot, { status, index }) => {
+    let item = flow.workflowActivityRules[index];
+
+    return (
+      <Popover
+        content={
+          <div>
+            节点类型:{ACTIVITY_TYPE[item.activityType]}
+            <br />
+            操作人类型:{ACTOR_TYPE[item.workflowActor?.actorType]}
+            <br />
+            审批类型:{APPROVAL_TYPE[item.workflowActor?.approvalType]}
+            <br />
+            审批方式:{APPROVAL_METHOD[item.workflowActor?.approvalMethod]}
+          </div>
+        }
+      >
+        {dot}
+      </Popover>
+    );
+  };
+
+  const getDetail = async () => {
+    if (!deptId || !userId) {
+      console.log('depId或userId不存在!');
+      return;
+    }
+    if (!timerRef.current.status) {
+      // 上锁
+      timerRef.current.status = true;
+      setLoading(true);
+      try {
+        let flow = await queryDDProcessesForecast({
+          processCode,
+          deptId,
+          userId,
+          formComponentValues,
+        });
+        setFlow(flow);
+        console.log(flow);
+      } catch (error) {}
+      setLoading(false);
+      setTimeout(() => {
+        // 延时解锁
+        timerRef.current.status = false;
+      }, 2000);
+    } else {
+      clearTimeout(timerRef.current.id);
+      // 延迟调用
+      timerRef.current.id = setTimeout(() => {
+        getDetail();
+      }, 2000);
+    }
+  };
+
+  useEffect(() => {
+    if (!processCode || !formComponentValues) return;
+    getDetail();
+  }, [processCode, formComponentValues]);
+
+  return (
+    <Spin spinning={loading}>
+      <div className={styles.top}>
+        <Steps current={current} progressDot={customDot} direction={direction}>
+          {flow.workflowActivityRules.map(item => (
+            <Step key={item.activityId} title={item?.activityName} description={getDesc(item)} />
+          ))}
+          {/* <Step key={item.activityId} title={item?.activityName} description={renderDesc(item)} /> */}
+        </Steps>
+        {showAduit && status != -1 && (
+          <div className={styles.btns} style={{ marginLeft: 80 }}>
+            <Button type="primary" onClick={() => onApprove(true, getTaskId())}>
+              审批通过
+            </Button>
+            <Button onClick={() => onApprove(false, getTaskId())} danger>
+              审批拒绝
+            </Button>
+          </div>
+        )}
+      </div>
+      {status !== undefined && renderAlert()}
+    </Spin>
+  );
+}
+
+const ACTOR_TYPE = {
+  approver: '审批人',
+  notifier: '抄送人',
+  audit: '办理人',
+};
+const APPROVAL_TYPE = {
+  MANUAL: '人工审批',
+  AUTO_AGREE: '自动通过',
+  AUTO_REFUSE: '自动拒绝',
+};
+const APPROVAL_METHOD = {
+  ONE_BY_ONE: '依次审批',
+  AND: '会签审批',
+  OR: '或签审批',
+};
+const ACTIVITY_TYPE = {
+  target_select: '自选审批人',
+  target_approval: '指定审批人',
+};
+const OPERATION_RESULT = {
+  AGREE: '同意',
+  REFUSE: '拒绝',
+  NONE: '未处理',
+};
+const OPERATION_TYPE = {
+  EXECUTE_TASK_NORMAL: '正常执行任务',
+  EXECUTE_TASK_AGENT: '代理人执行任务',
+  APPEND_TASK_BEFORE: '前加签任务',
+  APPEND_TASK_AFTER: '后加签任务',
+  REDIRECT_TASK: '转交任务',
+  START_PROCESS_INSTANCE: '发起流程实例',
+  TERMINATE_PROCESS_INSTANCE: '终止(撤销)流程实例',
+  FINISH_PROCESS_INSTANCE: '结束流程实例',
+  ADD_REMARK: '添加评论',
+  REDIRECT_PROCESS: '审批退回',
+  PROCESS_CC: '抄送',
+};
+
+const TASK_STATUS = {
+  NEW: '未启动',
+  RUNNING: '处理中',
+  PAUSED: '暂停',
+  CANCELED: '取消',
+  COMPLETED: '完成',
+  TERMINATED: '终止',
+};
+
+const TASK_RESULT = {
+  AGREE: '同意',
+  REFUSE: '拒绝',
+  REDIRECTED: '转交',
+  NONE: '未处理',
+};
+
+export default connect(({ user }) => ({
+  userList: user.list,
+}))(AuditFlow);

+ 112 - 0
src/pages/PurchaseAdmin/PurchaseList/Detail/HistoryDrawer.js

@@ -0,0 +1,112 @@
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import { UserOutlined } from '@ant-design/icons';
+import { Form } from '@ant-design/compatible';
+import '@ant-design/compatible/assets/index.css';
+import { Drawer, Descriptions, Card, Table, Timeline, Button, Space } from 'antd';
+import moment from 'moment';
+import { connect } from 'dva';
+import CommentContent from '@/components/CommentContent';
+import TimelineItem from 'antd/lib/timeline/TimelineItem';
+
+// 评论
+function HistoryDrawer(props) {
+  const {
+    flowDetail,
+    visible,
+    onClose,
+    version,
+    loading,
+    dispatch,
+    versionTree,
+    onChangeVersion,
+  } = props;
+  const columns = useMemo(() => {
+    return [
+      {
+        title: '名称',
+        render: item => (
+          <div style={{ color: '#9b9b9b' }}>
+            {item.version_no ? `${item.version_name}.${item.version_no}` : item.version_name}
+          </div>
+        ),
+      },
+      {
+        title: '业务节点',
+        width: '20%',
+        render: item => {
+          let node = flowDetail.Nodes.find(cur => cur.Id == item.template_node_id);
+          return <span style={{ color: '#9b9b9b' }}>{node?.label ? node.label : '-'}</span>;
+        },
+      },
+      {
+        title: '提交人',
+        width: '20%',
+        render: item => {
+          return (
+            <span style={{ color: '#9b9b9b' }}>
+              {item.AuthorInfo?.UserName ? item.AuthorInfo?.UserName : '-'}
+            </span>
+          );
+        },
+      },
+      {
+        title: '操作',
+        width: '20%',
+        render: item =>
+          item.id != version.id && (
+            <a
+              onClick={() => {
+                onChangeVersion(item);
+                onClose();
+              }}
+            >
+              加载
+            </a>
+          ),
+      },
+    ];
+  }, [version]);
+
+  const TimeLineItemRender = item => {
+    let time = item.m_time ? item.m_time.split('T')[0] : '-';
+    let node = flowDetail.Nodes.find(cur => cur.Id == item.template_node_id);
+    return (
+      <TimelineItem>
+        <Space>
+          <a
+            onClick={() => {
+              onChangeVersion(item);
+              onClose();
+            }}
+          >
+            {item.version_no ? `${item.version_name}.${item.version_no}` : item.version_name}
+          </a>
+          <span>{node?.label ? node.label : '-'}</span>
+          <span>提交人: {item.AuthorInfo?.UserName ? item.AuthorInfo?.UserName : '-'}</span>
+          <span>时间: {time}</span>
+        </Space>
+      </TimelineItem>
+    );
+  };
+
+  return (
+    <Drawer
+      width={600}
+      title="历史版本"
+      mask={false}
+      placement="right"
+      onClose={onClose}
+      visible={visible}
+    >
+      {/* <Table columns={columns} dataSource={versionTree} bordered={false} /> */}
+      <Timeline>{versionTree.map(item => TimeLineItemRender(item))}</Timeline>
+    </Drawer>
+  );
+}
+export default connect(({ detail, xflow, user, loading }) => ({
+  comment: detail.comment,
+  userList: user.list,
+  bomComment: detail.bomComment,
+  flowDetail: xflow.flowDetail,
+  loading: loading,
+}))(HistoryDrawer);

+ 176 - 0
src/pages/PurchaseAdmin/PurchaseList/Report/DepCompareModal.js

@@ -0,0 +1,176 @@
+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}
+        /> */}
+        {console.log(depCompare)}
+        {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);

+ 122 - 0
src/pages/PurchaseAdmin/PurchaseList/Report/Finance.js

@@ -0,0 +1,122 @@
+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 moment from 'moment';
+import { downloadFile, getToken } from '@/utils/utils.js';
+
+const { RangePicker } = DatePicker;
+
+function Finance(props) {
+  const { dispatch, loading, finance } = props;
+  const [form] = Form.useForm();
+  const [modalFilter, setModalFilter] = useState({});
+  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: 'type_name' },
+    { title: '保存未提交', dataIndex: 'un_audit_cnt' },
+    { title: '提交未审批', dataIndex: 'pending_audit_cnt' },
+    { title: '已拒绝', dataIndex: 'refuse_audit_cnt' },
+    { title: '审批通过', dataIndex: 'pass_audit_cnt' },
+  ];
+  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, project_name, project_status } = form.getFieldsValue();
+    filterRef.current.s_time = time[0] ? moment(time[0]).format('YYYY-MM-DD') : null;
+    filterRef.current.e_time = time[1] ? moment(time[1]).format('YYYY-MM-DD') : null;
+    filterRef.current.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 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}&project_name=${project_name}&project_status${project_status}`,
+      `财务报表${moment().format('YYYYMMDDHHMMSS')}.xlsx`
+    );
+  };
+  const renderSearch = () => {
+    return (
+      <Form layout="inline" form={form}>
+        <Form.Item label="时间" name="time" initialValue={[moment().startOf('years'), moment()]}>
+          <RangePicker placeholder="选择时间" allowClear={false} />
+        </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();
+  }, []);
+
+  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, loading }) => ({
+  finance: report.finance,
+  loading: loading.models.report,
+}))(Finance);