Renxy hai 1 ano
pai
achega
d57dc69011

+ 1 - 1
config/config.js

@@ -142,7 +142,7 @@ export default {
     '/api': {
       // target: 'http://192.168.20.152:8888/',
       // target: 'http://120.55.44.4:8896/',
-      target: 'http://47.96.12.136:8899/',
+      target: 'http://47.96.12.136:8888/',
       // target: 'http://oraysmart.com:8889/',
       // target: 'http://oraysmart.com:8888/api',
       // changeOrigin: true,

+ 139 - 0
src/components/charts/BarChartModule.js

@@ -0,0 +1,139 @@
+/*
+//y轴显示数据
+Data:{       
+  name:string,                       
+  data:string[],
+}
+
+props:{
+  xData:string[],  //x轴时间数据
+  dataList:Data[],  //数据列表
+}
+*/
+
+import { useEffect, useRef, useState } from 'react';
+import echarts from 'echarts';
+import styles from './index.less';
+import moment from 'moment';
+import { Empty } from 'antd';
+
+//图表模块
+const BarChartModule = props => {
+  const chartDomRef = useRef();
+  const chartRef = useRef();
+  const { xData, dataList } = props;
+  useEffect(() => {
+    chartRef.current = echarts.init(chartDomRef.current);
+    window.addEventListener('resize', resetChart);
+    return () => window.removeEventListener('resize', resetChart);
+  }, []);
+
+  useEffect(() => {
+    if (!chartRef.current || !dataList) return;
+    const option = { ...defaultOption };
+    option.xAxis.data = xData;
+    option.series = dataList.map((item, idx) => {
+      return {
+        ...option.series[idx],
+        ...item,
+      };
+    });
+    chartRef.current.clear();
+    chartRef.current.setOption(option);
+    chartRef.current.resize();
+  }, [xData, dataList]);
+
+  const resetChart = () => {
+    if (chartRef.current) chartRef.current.resize();
+  };
+
+  const getStyle = () => {
+    if (dataList && dataList.length != 0) return { width: '100%', height: '100%' };
+    else return { width: '100%', height: '100%', display: 'none' };
+  };
+
+  return (
+    <div className={styles.content}>
+      <div style={getStyle()} ref={chartDomRef} />
+      {(!dataList || dataList.length == 0) && <Empty />}
+    </div>
+  );
+};
+export default BarChartModule;
+const colors = [
+  '#5470c6',
+  '#91cc75',
+  '#fac858',
+  '#ee6666',
+  '#73c0de',
+  '#3ba272',
+  '#fc8452',
+  '#9a60b4',
+  '#ea7ccc',
+];
+const defaultOption = {
+  color: colors,
+  grid: {
+    bottom: 30,
+    left: 60,
+    right: 30,
+  },
+  xAxis: {
+    type: 'category',
+    axisTick: { show: false },
+    axisLine: {
+      lineStyle: {
+        color: '#c9d2d2',
+      },
+    },
+    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+  },
+  yAxis: {
+    type: 'value',
+    splitLine: {
+      lineStyle: {
+        type: 'dashed',
+      },
+    },
+    axisTick: { show: false },
+    axisLine: {
+      show: false,
+    },
+    axisLabel: {
+      color: '#fff',
+    },
+  },
+  legend: {
+    icon: 'circle',
+    right: '20%',
+    textStyle: {
+      color: '#fff',
+    },
+  },
+  series: [
+    {
+      name: '2022',
+      data: [120, 200, 150, 80, 70, 110, 130],
+      type: 'bar',
+      label: {
+        show: true,
+        position: 'top',
+        color: '#fff',
+      },
+      barGap: '0',
+      barMaxWidth: '10%',
+    },
+    {
+      name: '2023',
+      data: [120, 200, 150, 80, 70, 110, 130],
+      type: 'bar',
+      label: {
+        show: true,
+        position: 'top',
+        color: '#fff',
+      },
+      barGap: '0',
+      barMaxWidth: '10%',
+    },
+  ],
+};

+ 100 - 0
src/components/charts/PieChartModule.js

@@ -0,0 +1,100 @@
+/*
+data: [
+        { value: 1048, name: 'Search Engine' },
+        { value: 735, name: 'Direct' },
+        { value: 580, name: 'Email' },
+        { value: 484, name: 'Union Ads' },
+        { value: 300, name: 'Video Ads' },
+      ],
+*/
+
+import { useEffect, useRef, useState } from 'react';
+import echarts from 'echarts';
+import styles from './index.less';
+import moment from 'moment';
+import { Empty } from 'antd';
+
+//图表模块
+const PieChartModule = props => {
+  const chartDomRef = useRef();
+  const chartRef = useRef();
+  const { data } = props;
+  useEffect(() => {
+    chartRef.current = echarts.init(chartDomRef.current);
+    window.addEventListener('resize', resetChart);
+    return () => window.removeEventListener('resize', resetChart);
+  }, []);
+
+  useEffect(() => {
+    if (!chartRef.current || !data) return;
+    const option = { ...defaultOption };
+    option.series[0].data = data;
+    chartRef.current.clear();
+    chartRef.current.setOption(option);
+    chartRef.current.resize();
+  }, [data]);
+
+  const resetChart = () => {
+    if (chartRef.current) chartRef.current.resize();
+  };
+
+  const getStyle = () => {
+    if (data && data.length != 0) return { width: '100%', height: '100%' };
+    else return { width: '100%', height: '100%', display: 'none' };
+  };
+
+  return (
+    <div className={styles.content}>
+      <div style={getStyle()} ref={chartDomRef} />
+      {(!data || data.length == 0) && <Empty />}
+    </div>
+  );
+};
+export default PieChartModule;
+const colors = [
+  '#5470c6',
+  '#91cc75',
+  '#fac858',
+  '#ee6666',
+  '#73c0de',
+  '#3ba272',
+  '#fc8452',
+  '#9a60b4',
+  '#ea7ccc',
+];
+const defaultOption = {
+  color: colors,
+  tooltip: {
+    trigger: 'item',
+    formatter: '{b} : {d}% ({c})',
+  },
+  textStyle: {
+    color: '#fff',
+  },
+  series: [
+    {
+      type: 'pie',
+      radius: '70%',
+      data: [
+        { value: 1048, name: 'Search Engine' },
+        { value: 735, name: 'Direct' },
+        { value: 580, name: 'Email' },
+        { value: 484, name: 'Union Ads' },
+        { value: 300, name: 'Video Ads' },
+      ],
+      label: {
+        fontSize: 18,
+      },
+      emphasis: {
+        itemStyle: {
+          shadowBlur: 10,
+          shadowOffsetX: 0,
+          shadowColor: 'rgba(0, 0, 0, 0.5)',
+        },
+        label: {
+          fontSize: 24,
+        },
+      },
+    },
+  ],
+};

+ 63 - 0
src/components/charts/index.less

@@ -0,0 +1,63 @@
+.icon {
+  float: left;
+  width: 8px;
+  height: 30px;
+  background-color: #366cda;
+}
+.title {
+  color: #c9d2d2;
+  font-size: 22px;
+  padding-left: 14px;
+}
+.right {
+  color: #366cda;
+  float: right;
+  font-size: 20px;
+  cursor: default;
+}
+
+.leftArrow {
+  border: solid 20px;
+  border-color: transparent #366cda transparent transparent;
+}
+.rightArrow {
+  border: solid 20px;
+  border-color: transparent transparent transparent #366cda;
+}
+.typeList {
+  flex-grow: 1;
+  display: flex;
+}
+
+.content {
+  height: 100%;
+  :global {
+    .ant-tabs-nav-wrap {
+      background: none;
+    }
+    .ant-tabs-nav .ant-tabs-tab {
+      padding: 2px 16px;
+      background-color: #2196f330;
+      border: none;
+      margin: 0 6px;
+    }
+    .ant-tabs-tab-active {
+      color: #fff !important;
+      background-color: #366cda !important;
+    }
+    .ant-tabs-tab:hover {
+      color: #fff !important;
+    }
+    .ant-tabs-bar {
+      margin: 0;
+    }
+    // .ant-tabs-tab-prev-icon{
+    //   border: solid 20px ;
+    //   border-color: transparent #366CDA transparent  transparent ;
+    //   i{
+    //     width: 0;
+    //     height: 0;
+    //   }
+    // }
+  }
+}

+ 98 - 18
src/pages/PurchaseAdmin/PurchaseList/Approval/DetailModal.js

@@ -1,9 +1,13 @@
 import React, { useState, useEffect, useMemo, useRef } from 'react';
-import { Form, Modal, Steps, Tabs, TreeSelect } from 'antd';
+import { Button, Form, Modal, Select, Steps, Tabs, TreeSelect, Upload } from 'antd';
 import styles from './DetailModal.less';
 import TableRender from './TableRender';
 import MemberModal from './MemberModal';
 import StatusRender from './StatusRender';
+import { STATUS, SUB_STATUS } from './List';
+import { async } from '@antv/x6/lib/registry/marker/async';
+import { queryStatusHistory } from '@/services/approval';
+import { UploadOutlined } from '@ant-design/icons';
 
 const { Step } = Steps;
 // 新建
@@ -27,9 +31,42 @@ function DetailModal(props) {
     name: '',
     version: '',
   });
-  const [params, setParams] = useState({
-    sub_status: data.sub_status,
-  });
+  const [params, setParams] = useState({});
+  const [statusList, setStatusHistory] = useState([]);
+
+  const subSelectOptions = {
+    0: [
+      { value: 1, label: '预算和方式' },
+      { value: 7, label: '放弃' },
+      { value: 8, label: '失败' },
+      { value: 9, label: '关闭' },
+    ],
+    1: [
+      { value: 2, label: '招标' },
+      { value: 7, label: '放弃' },
+      { value: 8, label: '失败' },
+      { value: 9, label: '关闭' },
+    ],
+    2: [
+      { value: 3, label: '中标' },
+      { value: 8, label: '失败' },
+      { value: 9, label: '关闭' },
+    ],
+    3: [
+      { value: 4, label: '转执行' },
+      { value: 9, label: '关闭' },
+    ],
+    4: [
+      { value: 5, label: '转运营' },
+      { value: 6, label: '转质保' },
+      { value: 9, label: '关闭' },
+    ],
+    5: [
+      { value: 6, label: '转质保' },
+      { value: 9, label: '关闭' },
+    ],
+    6: [{ value: 9, label: '关闭' }],
+  };
 
   useEffect(() => {
     if (!visible || !data.id) return;
@@ -40,8 +77,37 @@ function DetailModal(props) {
       name: data.name,
       version: data.version,
     });
+    setParams({
+      project_status: data?.project_status,
+      sub_status: data?.status,
+    });
+    if (data?.id) initStatueHistory(data.id);
   }, [data, visible]);
 
+  const initStatueHistory = async id => {
+    const res = await queryStatusHistory({ id });
+    if (res.data) setStatusHistory(res.data);
+  };
+
+  const handleSubChange = e => {
+    console.log(e);
+    setParams({ ...params, sub_status: e });
+  };
+
+  const uploadProps = {
+    action: `/api/v2/approval/attach`,
+    headers: {
+      'JWT-TOKEN': localStorage.getItem('JWT-TOKEN'),
+    },
+    // defaultFileList: attachData?.attach_extend,
+    onChange({ file, fileList }) {
+      if (file.status !== 'uploading') {
+        const list = fileList.map(item => item.response?.data?.attach);
+        setParams({ ...params, attach: list });
+      }
+    },
+  };
+
   const renderDetail = () => (
     <>
       <div className={styles.subTitle}>项目详情</div>
@@ -73,12 +139,10 @@ function DetailModal(props) {
             </Form.Item>
           </>
         )}
-        {!isEdit && data.AuthorUser ? (
-          <Form.Item className={styles.formItem} label="售前项目经理">
-            {data.AuthorUser.CName}
-          </Form.Item>
-        ) : (
-          <Form.Item label="售前项目经理" className={styles.formItem} name="managerID">
+        <Form.Item className={styles.formItem} label="售前项目经理">
+          {!isEdit && data.AuthorUser ? (
+            data.AuthorUser.CName
+          ) : (
             <TreeSelect
               defaultValue={data.AuthorUser?.CName}
               showSearch
@@ -91,8 +155,8 @@ function DetailModal(props) {
               }}
               treeData={depUserTree}
             />
-          </Form.Item>
-        )}
+          )}
+        </Form.Item>
         {data.AuthorDepInfo && (
           <Form.Item className={styles.formItem} label="所属部门">
             {data.AuthorDepInfo.Name}
@@ -113,12 +177,28 @@ function DetailModal(props) {
             {data.OptManager.CName}
           </Form.Item>
         )}
-        {/* <Form.Item className={styles.formItem} label="项目阶段">
-          {data.OptManager.CName}
+        <Form.Item className={styles.formItem} label="项目阶段">
+          {STATUS.find(item => item.value == params?.project_status)?.label}
         </Form.Item>
         <Form.Item className={styles.formItem} label="现阶段状态">
-          {data.OptManager.CName}
-        </Form.Item> */}
+          {isEdit ? (
+            <Select
+              defaultValue={SUB_STATUS[0].label}
+              style={{ width: '60%' }}
+              onChange={handleSubChange}
+              options={data?.status || data?.status == 0 ? subSelectOptions[data?.status] : []}
+            />
+          ) : (
+            SUB_STATUS.find(item => item.value == data?.status)?.label
+          )}
+        </Form.Item>
+        {isEdit && data?.status >= 0 && data?.status <= 4 && (
+          <Form.Item className={styles.formItem} label="上传文件">
+            <Upload {...uploadProps}>
+              <Button icon={<UploadOutlined />}>上传文件</Button>
+            </Upload>
+          </Form.Item>
+        )}
         <Form.Item className={styles.formItem} label="项目规模">
           <TableRender
             onlyShow={true}
@@ -169,7 +249,7 @@ function DetailModal(props) {
     <Modal title="项目详情" width={800} visible={visible} onCancel={onClose} footer={null}>
       {renderDetail()}
       {/* {data.audit_status != 0 && renderAuth()} */}
-      <Tabs defaultActiveKey="3">
+      <Tabs defaultActiveKey="1">
         <Tabs.TabPane tab="成员管理" key="1">
           <MemberModal isEdit={isEdit} currentItem={data} />
         </Tabs.TabPane>
@@ -177,7 +257,7 @@ function DetailModal(props) {
           {renderAuth()}
         </Tabs.TabPane>
         <Tabs.TabPane tab="状态记录" key="3">
-          <StatusRender />
+          <StatusRender statusList={statusList} />
         </Tabs.TabPane>
       </Tabs>
     </Modal>

+ 50 - 39
src/pages/PurchaseAdmin/PurchaseList/Approval/List.js

@@ -28,24 +28,24 @@ import ProjectRecordModal from './ProjectRecordModal';
 
 const { Option } = Select;
 //项目阶段
-const SECTION = [
+export const STATUS = [
   { value: 0, label: '售前' },
   { value: 1, label: '执行' },
   { value: 2, label: '运营' },
   { value: 3, label: '质保' },
 ];
 //现阶段状态
-const STATUS = [
-  { value: 0, label: '初步交流' },
-  { value: 1, label: '预算和方式' },
-  { value: 2, label: '招标' },
-  { value: 3, label: '中标' },
-  { value: 4, label: '执行' },
-  { value: 5, label: '运营' },
-  { value: 6, label: '质保' },
-  { value: 7, label: '放弃' },
-  { value: 8, label: '失败' },
-  { value: 9, label: '关闭' },
+export const SUB_STATUS = [
+  { value: 1, label: '初步交流' },
+  { value: 2, label: '预算和方式' },
+  { value: 3, label: '招标' },
+  { value: 4, label: '中标' },
+  { value: 11, label: '执行' },
+  { value: 21, label: '运营' },
+  { value: 31, label: '质保' },
+  { value: 41, label: '失败' },
+  { value: 42, label: '放弃' },
+  { value: 43, label: '关闭' },
 ];
 
 function List(props) {
@@ -91,31 +91,33 @@ function List(props) {
       dataIndex: 'supplier_name',
     },
     {
-      title: '规模',
+      title: '工艺',
       dataIndex: 'process_info',
       render: info => {
-        const str = '';
+        let str = '';
         if (info) {
           const data = JSON.parse(info) || [];
-          const list = data.filter(item => item.scale);
+          const list = data.map(item => item.type);
           str = list.join('+');
         }
         return str;
       },
     },
     {
-      title: '工艺',
+      title: '规模',
       dataIndex: 'process_info',
       render: info => {
-        const str = '';
+        let str = '';
         if (info) {
           const data = JSON.parse(info) || [];
-          const list = data.filter(item => item.type);
+          console.log('-----------------', data);
+          const list = data.map(item => item.scale);
           str = list.join('+');
         }
         return str;
       },
     },
+
     {
       title: '项目种类',
       dataIndex: 'TypeInfo',
@@ -123,29 +125,23 @@ function List(props) {
     },
     {
       title: '项目阶段',
-      dataIndex: ['FlowInfo', 'name'],
+      dataIndex: 'project_status',
+      render: project_status => STATUS.find(item => item.value == project_status)?.label || '-',
     },
     {
       title: '现阶段状态',
-      dataIndex: 'project_status',
-      render: project_status => {
-        // return project_status === 0 ? <>售前</> : <>转执行</>;
-        //若添加其他状态则启用以下switch case:
-        switch (project_status) {
-          case 0:
-            return <>售前</>;
-          case 1:
-            return <>转执行</>;
-          case 2:
-            return <>转运营</>;
-          case 3:
-            return <>转质保</>;
-        }
-      },
+      dataIndex: 'status',
+      render: status => SUB_STATUS.find(item => item.value == status)?.label || '-',
     },
     {
-      title: '现阶段状态时间(月)',
-      render: () => '-',
+      title: '现阶段状态时间(天)',
+      dataIndex: 'current_status_start ',
+      align: 'center',
+      render: time => {
+        const date = moment(new Date());
+        const daysDiff = date.diff(time, 'days');
+        return daysDiff;
+      },
     },
     {
       title: '节点',
@@ -230,7 +226,22 @@ function List(props) {
           项目日志
         </a>
         <Divider type="vertical" />
-        <a onClick={() => {}}>设置人日预算</a>
+        <a
+          onClick={() => {
+            setCurrentItem(record);
+            dispatch({
+              type: 'approval/queryBudget',
+              payload: {
+                project_id: record?.id,
+              },
+              callback: () => {
+                setBudgetVisible(true);
+              },
+            });
+          }}
+        >
+          设置人日预算
+        </a>
       </>
     );
   };
@@ -284,7 +295,7 @@ function List(props) {
             }
           >
             <Option value={null}>全部</Option>
-            {SECTION.map(item => (
+            {STATUS.map(item => (
               <Option key={item.value}>{item.label}</Option>
             ))}
           </Select>
@@ -298,7 +309,7 @@ function List(props) {
             }
           >
             <Option value={null}>全部</Option>
-            {STATUS.map(item => (
+            {SUB_STATUS.map(item => (
               <Option key={item.value}>{item.label}</Option>
             ))}
           </Select>

+ 44 - 3
src/pages/PurchaseAdmin/PurchaseList/Approval/Statistic.js

@@ -1,24 +1,65 @@
+import { useEffect, useRef } from 'react';
 import styles from './index.less';
+import echarts from 'echarts';
+import BarChartModule from '@/components/charts/BarChartModule';
+import { Radio } from 'antd';
 const Statistic = () => {
+  const options = [
+    {
+      label: '月',
+      value: 0,
+    },
+    {
+      label: '季度',
+      value: 1,
+    },
+    {
+      label: '年',
+      value: 2,
+    },
+  ];
   return (
     <div className={styles.statistic}>
       <div className={styles.boxCon}>
         <div style={{ fontSize: '22px' }}>项目统计</div>
         <div style={{ display: 'flex', width: '100%', justifyContent: 'space-around' }}>
           <div style={{ textAlign: 'center' }}>
-            <div style={{ color: 'red', fontSize: '32px' }}>110</div>
+            <div style={{ color: '#f5a41f', fontSize: '32px' }}>110</div>
             <div>大部分看开点</div>
           </div>
           <div style={{ textAlign: 'center' }}>
-            <div style={{ color: 'red', fontSize: '32px' }}>110</div>
+            <div style={{ color: '#f5a41f', fontSize: '32px' }}>110</div>
             <div>大部分看开点</div>
           </div>
           <div style={{ textAlign: 'center' }}>
-            <div style={{ color: 'red', fontSize: '32px' }}>110</div>
+            <div style={{ color: '#f5a41f', fontSize: '32px' }}>110</div>
             <div>大部分看开点</div>
           </div>
         </div>
       </div>
+      <div style={{ display: 'flex', marginTop: '26px', justifyContent: 'space-between' }}>
+        <div className={styles.boxCon} style={{ width: '49.2%' }}>
+          <div style={{ fontSize: '22px' }}>项目状态统计</div>
+          <div style={{ height: '300px' }}>
+            <BarChartModule />
+          </div>
+        </div>
+        <div className={styles.boxCon} style={{ width: '49.2%', position: 'relative' }}>
+          <div style={{ fontSize: '22px' }}>项目分类统计</div>
+          <div style={{ position: 'absolute', top: '16px', right: '20px' }}>
+            <Radio.Group
+              options={options}
+              onChange={() => {}}
+              value={0}
+              optionType="button"
+              buttonStyle="solid"
+            />
+          </div>
+          <div style={{ height: '300px' }}>
+            <BarChartModule />
+          </div>
+        </div>
+      </div>
     </div>
   );
 };

+ 13 - 8
src/pages/PurchaseAdmin/PurchaseList/Approval/StatusRender.js

@@ -1,16 +1,21 @@
 import { Timeline } from 'antd';
 import styles from './index.less';
+import { STATUS, SUB_STATUS } from './List';
+import moment from 'moment';
 
-const StatusRender = () => {
+const StatusRender = ({ statusList }) => {
   return (
     <Timeline>
-      <Timeline.Item dot={<div className={styles.icon}>1</div>}>
-        <div style={{ fontSize: '16px', color: '#1890ff' }}>售前经理</div>
-        <div>售前Solve initial network problems 2015-09-01经理</div>
-      </Timeline.Item>
-      <Timeline.Item>Solve initial network problems 2015-09-01</Timeline.Item>
-      <Timeline.Item>Technical testing 2015-09-01</Timeline.Item>
-      <Timeline.Item>Network problems being solved 2015-09-01</Timeline.Item>
+      {statusList?.map(item => (
+        <Timeline.Item dot={<div className={styles.icon}>1</div>}>
+          <div style={{ fontSize: '16px', color: '#1890ff' }}>{`${
+            STATUS.find(cur => cur.value == item.project_status)?.label
+          }-${SUB_STATUS.find(cur => cur.value == item.status)?.label}`}</div>
+          <div>{`持续时间:${item.continuously}    修改人员:${author_name}   修改时间:${moment(
+            item.c_time
+          ).format('YYYY-MM-DD')}`}</div>
+        </Timeline.Item>
+      ))}
     </Timeline>
   );
 };

+ 1 - 1
src/pages/PurchaseAdmin/PurchaseList/Approval/TableRender.js

@@ -64,7 +64,7 @@ const TableRender = ({ value, onChange, onlyShow = false }) => {
   return (
     <div>
       {onlyShow ? (
-        <div className={styles.content}>
+        <div className={styles.content} style={{ width: '60%' }}>
           {list.map((item, idx) => renderOnlyShowItems(item, idx))}
         </div>
       ) : (

+ 1 - 1
src/pages/PurchaseAdmin/PurchaseList/Approval/index.less

@@ -70,6 +70,6 @@
     width: 100%;
     padding: 10px;
     border-radius: 12px;
-    box-shadow: 0px 0px 20px 4px rgba(0, 0, 0, 0.3);
+    box-shadow: 0px 0px 10px 4px rgba(0, 0, 0, 0.2);
   }
 }

+ 5 - 0
src/services/approval.js

@@ -134,3 +134,8 @@ export async function modifyManager(params) {
     body: params,
   });
 }
+
+//历史状态接口
+export async function queryStatusHistory() {
+  return request(`/api/v2/approval/status/history`);
+}