Browse Source

feat: 新增合同管理-收入合同页面

lanjianrong 4 years ago
parent
commit
2f1468bc2c

+ 5 - 1
package.json

@@ -75,6 +75,7 @@
       "react-app"
     ],
     "plugins": [
+      "react-activation/babel",
       [
         "import",
         {
@@ -173,14 +174,17 @@
     "workbox-webpack-plugin": "4.3.1"
   },
   "dependencies": {
-    "react-activation": "^0.5.5",
     "@ant-design/icons": "^4.2.2",
+    "@grapecity/spread-sheets": "^14.0.0",
+    "@grapecity/spread-sheets-react": "^14.0.0",
     "antd": "^4.6.4",
     "axios": "^0.20.0",
     "mobx": "^6.0.1",
     "mobx-react": "^7.0.0",
+    "moment": "^2.29.1",
     "nprogress": "^0.2.0",
     "react": "^17.0.1",
+    "react-activation": "^0.5.5",
     "react-dom": "^16.13.1",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0"

+ 73 - 3
src/assets/css/common.scss

@@ -223,23 +223,82 @@
   text-align: right;
 }
 
+/*************************
+定位
+**************************/
+.pi-absolute {
+  position: absolute;
+}
+.pi-relative {
+  position: relative;
+}
+.pi-fixed {
+  position: fixed;
+}
+
+// absolute
+.pi-absolute-top {
+  @extend .pi-absolute;
+
+  top: 0;
+  left: 0;
+  z-index: 2;
+}
+.pi-absolute-center {
+  @extend .pi-absolute;
+
+  top: 50%;
+  left: 50%;
+  z-index: 2;
+  transform: translate(-50%, -50%);
+}
+
+.pi-absolute-lt {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 2;
+}
+.pi-absolute-rt {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 2;
+}
+.pi-absolute-lb {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  z-index: 2;
+}
+.pi-absolute-rb {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  z-index: 2;
+}
+
 // 公共头部
 .pi-header {
   @extend .pi-width-100P;
+  @extend .pi-relative;
+  text-align: center;
   background: linear-gradient(#ccc, 2%, #ffffff);
   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
   border-bottom: 1px solid #ddd;
 
   .pi-header-left {
-    @extend .pi-text-left;
+    @extend .pi-absolute-lt;
+    left: 100px;
   }
 
   .pi-header-center {
-    @extend .pi-text-center;
+    @extend .pi-absolute-center;
   }
 
   .pi-header-right {
-    @extend .pi-text-right;
+    @extend .pi-absolute-rt;
+    right: 20px;
   }
 }
 
@@ -248,3 +307,14 @@
   flex-direction: column;
   width: calc(100vw - 55px - 120px);
 }
+
+// 按钮
+.pi-btn-secondary {
+  color: #fff;
+  background-color: #6c757d;
+  border-color: #6c757d;
+}
+
+.pi-gray-color {
+  color: #757575;
+}

+ 6 - 4
src/components/Header/index.tsx

@@ -1,6 +1,6 @@
 import React, { PropsWithChildren } from 'react'
 interface iHeaderProps {
-  title: string;
+  title?: string;
 }
 
 
@@ -27,10 +27,12 @@ const Header: React.FC<PropsWithChildren<iHeaderProps>> = props => {
     )
   }
   return (
-    <div className="pi-height-34 pi-lh-30 pi-pd-right-20 pi-pd-left-15 pi-justify-between pi-align-center pi-header">
-      <span>{props.title}</span>
+    <div className="pi-height-34 pi-lh-30 pi-pd-right-20 pi-pd-left-15 pi-header pi-flex-row">
+      {
+        props.title ? <span>{props.title}</span> : ''
+      }
       {props.children && Array.isArray(props.children) ?
-        props.children.map((child, index) => renderChild(child, index)) : props.children && renderChild(props.children)}
+          props.children.map((child, index) => renderChild(child, index)) : props.children && renderChild(props.children)}
     </div>
   )
 }

+ 1 - 1
src/components/Header/slot.tsx

@@ -1,7 +1,7 @@
 import React, { PropsWithChildren } from 'react'
 declare type PositionType = 'left' | 'center' | 'right' | undefined
 interface iSlotProps {
-  position: PositionType
+  position?: PositionType
 }
 
 const Slot:React.FC<PropsWithChildren<iSlotProps>> = (props) => {

+ 5 - 1
src/components/LeftSide/index.tsx

@@ -1,16 +1,20 @@
+import { tenderState } from '@/store/mobx'
 import { iNavSide } from '@/types/router'
 import React from 'react'
 import { Link } from 'react-router-dom'
 import "./index.scss"
 
 const leftSide:React.FC<iNavSide> = ({ childRoutes, location }) => {
+  if (location.state?.id) {
+    tenderState.saveBidsectionId(location.state?.id)
+  }
   const pathname = location.pathname
   return (
     <div className="panel-sidebar">
       <div className="pi-pd-10 pi-mg-bottom-10 sidebar-title">项目设置</div>
       <div className="pi-flex-column pi-justify-start">
         { childRoutes.map((item, idx) =>
-          item.meta && <Link key={idx} to={item.path} className={pathname.indexOf(item.path) !== -1 ? 'nav-link active' : 'nav-link'}>{item.meta.title}</Link>
+          item.meta && <Link key={idx} to={{ pathname: item.path, state: { id: location.state?.id } }} className={pathname.indexOf(item.path) !== -1 ? 'nav-link active' : 'nav-link'}>{item.meta.title}</Link>
         )}
       </div>
     </div>

+ 7 - 3
src/components/Menu/index.tsx

@@ -28,9 +28,13 @@ class NavSider extends Component<iMenuProps, any> {
           </div>
           <div className="nav-footer">
             {
-              MeunList?.map((item: iMenuItem, idx: number) =>
-                !item.isTop && <MenuItem key={idx} item={item}></MenuItem>
-              )
+              MeunList?.map((item: iMenuItem, idx: number) => {
+                if (item.path === '/console/management') {
+                  return userState.role === 'ADMIN' ? !item.isTop && <MenuItem key={idx} item={item}></MenuItem> : ''
+                } else {
+                  return !item.isTop && <MenuItem key={idx} item={item}></MenuItem>
+                }
+              })
             }
             <div className="pi-text-center">
               <Dropdown overlay={() => {

+ 6 - 0
src/pages/Contract/Content/Income/api.ts

@@ -0,0 +1,6 @@
+import request from '@/utils/common/request'
+
+export async function apiContractSheet() {
+  const { data } = await request.get('/api/contract/income')
+  return data
+}

+ 118 - 0
src/pages/Contract/Content/Income/components/ContractModal.tsx

@@ -0,0 +1,118 @@
+import { iModalCommonProps } from '@/types/contract'
+import { Button, DatePicker, Form, Input, message, Modal, Select, TreeSelect } from 'antd'
+import locale from 'antd/es/date-picker/locale/zh_CN'
+import 'moment/locale/zh-cn'
+import React from 'react'
+const ContractModal: React.FC<iModalCommonProps> = ({ modalObj: { type, visible, confirmLoading }, onConfirm, onCancel }) => {
+  const { Option } = Select
+  const [ form ] = Form.useForm()
+  const modalObj = {
+    add: {
+      title: '新建合同',
+      cancalText: '取消',
+      okText: '确认添加'
+    },
+    edit: {
+      title: '编辑合同',
+      cancalText: '取消',
+      okText: '确认'
+    },
+    close: {
+      title: '关闭合同',
+      cancalText: '取消',
+      okText: '确认关闭'
+    }
+  }
+  return (
+    <Modal
+      visible={visible}
+      title={modalObj[type]?.title}
+      onCancel={onCancel}
+      footer={[
+        type === 'edit' ? <Button type="primary" key="delete" size="small" danger >删除合同</Button> : '',
+        <Button type="primary" size="small" key="cancel" className="pi-btn-secondary" onClick={onCancel}>{modalObj[type]?.cancalText}</Button>,
+        <Button type="primary" size="small" key="ok" loading={confirmLoading} onClick={() => {
+          form.validateFields().then(values => {
+            form.resetFields()
+            onConfirm(values, type)
+          }).catch(info => {
+            message.error('Validate Failed:', info)
+          })
+        }}>{modalObj[type]?.okText}</Button>
+    ]}
+    >
+      <Form form={form} layout='vertical' style={{ maxHeight: '482px', overflowY: 'scroll' }}>
+        {
+          type === 'add' ? (
+          <>
+            <Form.Item name="id1" label="合同劳务">
+              <TreeSelect></TreeSelect>
+            </Form.Item>
+            <Form.Item name="id2" label="合同编号" rules={[ { required: true, message: '请输入合同编号' } ]}>
+              <Input addonAfter={<span>自动编号</span>}></Input>
+            </Form.Item>
+            <Form.Item name="id3" label="合同名称" rules={[ { required: true, message: '请输入合同名称' } ]}>
+              <Input placeholder="输入合同名称"></Input>
+            </Form.Item>
+            <Form.Item name="id4" label="合同类型" rules={[ { required: true, message: '请选择合同类型' } ]}>
+              <Select showSearch>
+                <Option value="0">支出合同</Option>
+                <Option value="1">收入合同</Option>
+              </Select>
+            </Form.Item>
+            <Form.Item name="id5" label="合同金额" rules={[ { required: true, message: '请输入合同金额' } ]}>
+              <Input placeholder="输入合同金额" addonAfter={<span>元</span>}></Input>
+            </Form.Item>
+          </>
+          ) : ''
+        }
+        {
+          type === 'edit' ? (
+            <>
+              <Form.Item name="id1" label="合同项目">
+                <TreeSelect></TreeSelect>
+              </Form.Item>
+              <Form.Item name="id2" label="序号" rules={[ { required: true, message: '请输入序号' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id3" label="项目内容" rules={[ { required: true, message: '请输入项目内容' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id4" label="项目金额" rules={[ { required: true, message: '请输入项目金额' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id5" label="甲方" rules={[ { required: true, message: '请输入甲方' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id6" label="甲方签约人" rules={[ { required: true, message: '请输入甲方签约人' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id7" label="乙方" rules={[ { required: true, message: '请输入乙方' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id8" label="乙方签约人" rules={[ { required: true, message: '请输入乙方签约人' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="id9" label="合同签约日期" rules={[ { required: true, message: '请选择签约日期' } ]}>
+                <DatePicker allowClear locale={locale}></DatePicker>
+              </Form.Item>
+              <Form.Item name="id10" label="备注">
+                <Input.TextArea maxLength={100}></Input.TextArea >
+              </Form.Item>
+            </>
+          ) : ''
+        }
+        {
+          type === 'close' ? (
+            <>
+              <Form.Item name="id" hidden><Input></Input></Form.Item>
+              <span>关闭后,合同将锁定,无法进行编辑、上传文件等操作。</span>
+            </>
+          ) : ''
+        }
+      </Form>
+    </Modal>
+  )
+}
+
+export default ContractModal

+ 48 - 0
src/pages/Contract/Content/Income/components/GCsheet.module.scss

@@ -0,0 +1,48 @@
+.spreadContent {
+  position: relative;
+  height: calc(100vh - 34px);
+  overflow: hidden;
+  .spreadSheets {
+    width: 100%;
+    height: 62%;
+  }
+  .extraControl {
+    width: 100%;
+    height: 38%;
+  }
+}
+.modalWarnText {
+  padding: 0.5rem;
+  margin-bottom: 1rem;
+  color: #856404;
+  background-color: #fff3cd;
+  border: 1px solid transparent;
+  border-color: #ffeeba;
+  border-radius: 0.25rem;
+}
+
+.modalTemplateContent {
+  display: flex;
+  justify-content: space-between;
+  & > div {
+    background-color: #ffffff;
+    background-clip: border-box;
+    border: 1px solid rgba(0, 0, 0, 0.125);
+    border-radius: 0.25rem;
+    &:hover {
+      border-color: #007bff;
+    }
+  }
+  .leftTemplate {
+    width: 45%;
+  }
+  .rightTemplate {
+    width: 45%;
+  }
+}
+
+.projectTable {
+  :global(.ant-table-cell) {
+    padding: 0.3rem;
+  }
+}

+ 215 - 0
src/pages/Contract/Content/Income/components/GCsheet.tsx

@@ -0,0 +1,215 @@
+import { tenderState } from '@/store/mobx'
+import consts from '@/utils/consts'
+import GC from '@grapecity/spread-sheets'
+import { SpreadSheets, Worksheet } from '@grapecity/spread-sheets-react'
+import { Button, message, Radio, Table, Tabs } from 'antd'
+import Modal from 'antd/lib/modal/Modal'
+import { RadioChangeEvent } from 'antd/lib/radio'
+import React, { useEffect, useState } from 'react'
+import { apiContractIncome, apiSetTemplate } from './api'
+import styles from './GCsheet.module.scss'
+GC.Spread.Common.CultureManager.culture("zh-cn")
+interface iGCsheetPorps {
+  modalHandler: (type: string) => void
+}
+interface iTemplateState {
+  attribution: string
+  children: iTemplateState[] | undefined
+  depth: number
+  id: number
+  isEnd: boolean
+  leaf: boolean
+  name: string
+  parentId: number
+  serial: string
+}
+interface iShowTemplateState {
+  isShow: boolean
+  template: string
+  loading: boolean
+}
+const GCsheet: React.FC<iGCsheetPorps> = ({ modalHandler }) => {
+  const [ sectionTemplate, setSectionTemplate ] = useState<iShowTemplateState>({
+    isShow: false,
+    template: '',
+    loading: false
+  })
+  const [ projectTree, setProjectTree ] = useState<{}>({})
+  const [ tempalte, setTempalte ] = useState<{template1: iTemplateState, template2: iTemplateState}>({
+    template1: {
+      attribution: '',
+      children: undefined,
+      depth: 0,
+      id: 0,
+      isEnd: false,
+      leaf: false,
+      name: '',
+      parentId: 0,
+      serial: ''
+    },
+    template2: {
+      attribution: '',
+      children: undefined,
+      depth: 0,
+      id: 0,
+      isEnd: false,
+      leaf: false,
+      name: '',
+      parentId: 0,
+      serial: ''
+    }
+  })
+  const { TabPane } = Tabs
+  const initSpread = (spread: GC.Spread.Sheets.Workbook) => {
+    spread.suspendPaint()
+    spread.resumePaint()
+  }
+  useEffect(() => {
+    initHandler()
+  }, [])
+  const initHandler = async () => {
+    const data  = await apiContractIncome(tenderState.bidsectionId)
+    if (data.code === consts.RET_CODE.SUCCESS) {
+      if (data.isTemplate && data.isTemplate === 1) {
+        setSectionTemplate({
+          ...sectionTemplate,
+          isShow: true
+        })
+        setTempalte({
+          ...tempalte,
+          template1: data.sectionTemplate1,
+          template2: data.sectionTemplate2
+        })
+      } else {
+        setProjectTree(data.data)
+      }
+    }
+  }
+  const columns = [
+    {
+      title: '项目节',
+      dataIndex: 'serial',
+      width: '40%',
+      // eslint-disable-next-line react/display-name
+      render: (text: string, row: iTemplateState) => {
+        const { attribution = '', serial = '' } = row
+        return <span>{`${attribution}${serial}`}</span>
+      }
+    },
+    {
+      title: '名称',
+      dataIndex: 'name',
+      width: '60%'
+    }
+  ]
+  // modal 确认 - 回调
+  const handleModalConfirm = async () => {
+    setSectionTemplate({
+      ...sectionTemplate,
+      loading: true
+    })
+    if (!sectionTemplate.template) return message.error('请选择项目节模板!')
+    const { code = -1 } = await apiSetTemplate(sectionTemplate.template, tenderState.bidsectionId)
+    if (code === consts.RET_CODE.SUCCESS) {
+      await initHandler()
+    }
+    setSectionTemplate({
+      ...sectionTemplate,
+      loading: false,
+      isShow: false
+    })
+  }
+  // radio 切换回调
+  const handleRadioEvent = (e: RadioChangeEvent) => {
+    if (e.target.checked) {
+      setSectionTemplate({
+        ...sectionTemplate,
+        template: e.target.value
+      })
+    }
+
+  }
+  return sectionTemplate.isShow ?
+  <Modal
+    visible={sectionTemplate.isShow}
+    maskClosable={false}
+    title="选择合同项目节模板"
+    okText="确定"
+    confirmLoading={sectionTemplate.loading}
+    cancelText="关闭"
+    closable={false}
+    keyboard={false}
+    onOk={() => handleModalConfirm()}
+    width='70vw'
+  >
+    <div className={styles.modalWarnText}>默认项目节无法修改,可自行增加维护子节点</div>
+    <div className={styles.modalTemplateContent}>
+      <div className={styles.leftTemplate}>
+        <div className="pi-pd-20">
+          <Radio value="1" checked={sectionTemplate.template === '1'} onChange={(e: RadioChangeEvent) => handleRadioEvent(e)}><span className="pi-gray-color">项目节模板1</span></Radio>
+        </div>
+        <div className={styles.projectTable}>
+          {
+            tempalte.template1?.children &&  tempalte.template1?.children.length?
+            <Table
+            dataSource={tempalte.template1?.children}
+            columns={columns}
+            pagination={false}
+            scroll={{ y: '300px' }}
+            rowKey={record => record.id}
+            defaultExpandAllRows={true}
+            >
+          </Table> : ''
+          }
+        </div>
+
+      </div>
+      <div className={styles.rightTemplate}>
+        <div className="pi-pd-20 pi-gray-color">
+          <Radio value="2" checked={sectionTemplate.template === '2'} onChange={(e: RadioChangeEvent) => handleRadioEvent(e)}><span className="pi-gray-color">项目节模板2</span></Radio>
+        </div>
+        <div className={styles.projectTable}>
+          {
+            tempalte.template2?.children &&  tempalte.template2?.children.length? <Table
+            dataSource={tempalte.template2?.children}
+            columns={columns}
+            pagination={false}
+            scroll={{ y: '300px' }}
+            rowKey={record => record.id}
+            defaultExpandAllRows={true}
+            >
+          </Table> : ''
+          }
+
+        </div>
+      </div>
+    </div>
+  </Modal>
+  :
+  <div className={styles.spreadContent}>
+    <div className={styles.spreadSheets}>
+      <SpreadSheets
+        workbookInitialized={(spread: GC.Spread.Sheets.Workbook)=> initSpread(spread)}
+        showHorizontalScrollbar={false}
+        showVerticalScrollbar={false}
+        tabStripVisible={false}
+        >
+        <Worksheet rowHeaderVisible={false}>
+        </Worksheet>
+    </SpreadSheets>
+    </div>
+    <div className={styles.extraControl}>
+      <Tabs
+        type="card"
+        size="small"
+        defaultActiveKey="1"
+        tabBarExtraContent={{ right: <div className="pi-mg-right-5"><Button type="primary" size="small" danger className="pi-mg-right-3" onClick={() => modalHandler('close')}>关闭合同</Button><Button type="primary" size="small" onClick={() => modalHandler('edit')}>编辑合同</Button></div> }}>
+        <TabPane key="1" tab="合同详情"></TabPane>
+        <TabPane key="2" tab="合同回款"></TabPane>
+        <TabPane key="3" tab="合同文件"></TabPane>
+      </Tabs>
+    </div>
+  </div>
+}
+
+export default GCsheet

+ 11 - 0
src/pages/Contract/Content/Income/components/api.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/common/request'
+
+export async function apiContractIncome(bidsectionId: string) {
+  const { data } = await request.get('/api/contract/income', { bidsectionId })
+  return data
+}
+
+export async function apiSetTemplate(templateNumber: string, bidsectionId: string) {
+  const { data } = await request.post('/api/contract/section/template', { templateNumber: parseInt(templateNumber), bidsectionId })
+  return data
+}

+ 86 - 0
src/pages/Contract/Content/Income/index.tsx

@@ -0,0 +1,86 @@
+import Header from '@/components/Header'
+import Slot from '@/components/Header/slot'
+import { tenderState } from '@/store/mobx'
+import { iModalBooleanProps } from '@/types/contract'
+import consts from '@/utils/consts'
+import { ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined, CloseOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'
+import '@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'
+import { Button, Tooltip } from 'antd'
+import React, { useState } from 'react'
+import { apiContractSheet } from './api'
+import ContractModal from './components/ContractModal'
+import GCsheet from './components/GCsheet'
+
+export default function Income() {
+  console.log(tenderState.bidsectionId)
+
+  const [ modalObj, setModalObj ] = useState<iModalBooleanProps>({
+    type: '',
+    visible: false,
+    confirmLoading: false
+  })
+
+  const onCreate = async (value: any, type: any) => {
+    setModalObj({
+      ...modalObj,
+      confirmLoading: true
+    })
+    const { code = -1 } = await apiContractSheet()
+    if (code === consts.RET_CODE.SUCCESS) {
+      //
+    }
+    setModalObj({
+      ...modalObj,
+      type: '',
+      visible: false,
+      confirmLoading: false
+    })
+  }
+  const modalHandler = (type: string) => {
+    setModalObj({
+      ...modalObj,
+      visible: true,
+      type
+    })
+  }
+  return (
+    <div className="content-wrap">
+      <Header title="维护项目节:">
+        <Slot position="left">
+          <div className="pi-flex-row">
+              <Tooltip title="添加子项">
+                <Button type="text" icon={<PlusOutlined style={{ color: '#007bff' }}/>}></Button>
+              </Tooltip>
+
+              <Tooltip title="删除">
+                <Button type="text" icon={<CloseOutlined style={{ color: '#007bff' }}/>}/>
+              </Tooltip>
+
+              <Tooltip title="左移">
+                <Button type="text" icon={<ArrowLeftOutlined />} style={{ color: '#007bff' }}></Button>
+              </Tooltip>
+
+              <Tooltip title="右移">
+                <Button type="text" icon={<ArrowRightOutlined style={{ color: '#007bff' }}/>}></Button>
+              </Tooltip>
+
+            <Tooltip title="下移">
+              <Button type="text" icon={<ArrowUpOutlined style={{ color: '#007bff' }}/>}></Button>
+              </Tooltip>
+
+              <Tooltip title="上移">
+                <Button type="text" icon={<ArrowDownOutlined style={{ color: '#007bff' }}/>}></Button>
+              </Tooltip>
+          </div>
+        </Slot>
+        <Slot position="right">
+          <Button type="ghost" size="small" icon={<SettingOutlined />} className="pi-mg-right-3" style={{ color: '#007bff' }}>设置</Button>
+          <Button type="primary" size="small" onClick={() => setModalObj({ ...modalObj, type: 'add', visible: true })}>新建收入合同</Button>
+        </Slot>
+      </Header>
+      <GCsheet modalHandler={modalHandler}></GCsheet>
+      <ContractModal modalObj={modalObj} onConfirm={onCreate} onCancel={ () => setModalObj({ ...modalObj, visible: false })}></ContractModal>
+    </div>
+  )
+}
+

+ 1 - 1
src/pages/Contract/Spending/index.tsx

@@ -3,7 +3,7 @@ import React from 'react'
 export default function Spending() {
   return (
     <div>
-
+<h5>111</h5>
     </div>
   )
 }

+ 1 - 1
src/pages/Contract/Summary/index.tsx

@@ -3,7 +3,7 @@ import React from 'react'
 export default function Summary() {
   return (
     <div>
-
+<h5>111</h5>
     </div>
   )
 }

+ 20 - 0
src/pages/Contract/Content/index.tsx

@@ -0,0 +1,20 @@
+import LeftSide from '@/components/LeftSide'
+import Guards from '@/components/Navigation'
+import { NavigationGuardsProps } from '@/types/router'
+import React from 'react'
+import { Switch } from 'react-router-dom'
+const Content:React.FC<NavigationGuardsProps> = props => {
+  const { routeConfig, match, location } = props
+  return (
+    <>
+      <LeftSide childRoutes={routeConfig} location={location}></LeftSide>
+      <div className="panel-content">
+        <Switch>
+              <Guards routeConfig={routeConfig} match={match} location={location}></Guards>
+        </Switch>
+      </div>
+    </>
+  )
+}
+
+export default Content

+ 0 - 9
src/pages/Contract/Income/index.tsx

@@ -1,9 +0,0 @@
-import React from 'react'
-
-export default function Income() {
-  return (
-    <div>
-
-    </div>
-  )
-}

+ 6 - 0
src/pages/Contract/List/api.ts

@@ -0,0 +1,6 @@
+import request from "@/utils/common/request"
+
+export async function apiContractList() {
+  const { data } = await request.get('/api/contract')
+  return data
+}

+ 35 - 0
src/pages/Contract/List/index.module.scss

@@ -0,0 +1,35 @@
+.tableContent {
+  margin: 0;
+  font-size: 12px;
+
+  .treeBtn {
+    padding: 1px 0.6rem;
+    color: #007bff;
+    border: 1px solid #007bff;
+    border-radius: 0.2rem;
+    &:hover {
+      color: #ffffff;
+      background-color: #007bff;
+    }
+  }
+
+  :global(.ant-table-thead .ant-table-cell) {
+    padding: 0.4rem;
+    text-align: center !important;
+    background-color: #e9ecef;
+    border-right: 1px solid #dee2e6;
+    border-bottom: 2px solid #dee2e6;
+  }
+  :global(.ant-table-tbody .ant-table-cell) {
+    padding: 0.4rem;
+  }
+  :global(.ant-table.ant-table-bordered > .ant-table-container > .ant-table-content > table > thead > tr > th) {
+    border-right: 1px solid #dee2e6;
+  }
+}
+
+.textBtn {
+  &:hover {
+    color: #007bff;
+  }
+}

+ 3 - 0
src/pages/Contract/List/index.scss

@@ -0,0 +1,3 @@
+.list-content {
+  width: calc(100vw - 55px);
+}

+ 136 - 0
src/pages/Contract/List/index.tsx

@@ -0,0 +1,136 @@
+import Header from '@/components/Header'
+import Slot from '@/components/Header/slot'
+import SvgIcon from '@/components/SvgIcon'
+import { ContractTree } from '@/types/contract'
+import consts from '@/utils/consts'
+import { CaretDownOutlined } from '@ant-design/icons'
+import { Button, Dropdown, Menu, message, Table } from 'antd'
+import { ColumnsType } from 'antd/lib/table'
+import React, { useEffect, useState } from 'react'
+import { useActivate } from 'react-activation'
+import { RouteComponentProps } from 'react-router'
+import { Link } from 'react-router-dom'
+import { apiContractList } from './api'
+import styles from './index.module.scss'
+import './index.scss'
+const List: React.FC<RouteComponentProps> = () => {
+  const [ tree, setTree ] = useState({
+    bidsectionId: '',
+    children: [],
+    childsTotal: 0,
+    contracts: 0,
+    contractsIncome: '',
+    contractsIncomeProgress: '',
+    contractsPaid: '',
+    contractsPay: '',
+    contractsPayProgress: '',
+    contractsReturned: '',
+    csrf: '',
+    hasFolder: false,
+    id: '',
+    isBid: false,
+    isEnd: false,
+    isfolder: 0,
+    name: '',
+    parentId: '',
+    projectId: ''
+  })
+  const getTree = async () => {
+    const { data = {}, code = -1 } = await apiContractList()
+    if (code === consts.RET_CODE.SUCCESS) {
+      setTree(data)
+    }
+  }
+  const columns:ColumnsType<ContractTree> = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      key: 'name',
+      width: '25%',
+      render: (text: string, record: ContractTree) => {
+        if (record.isfolder) {
+          return <div style={{ verticalAlign: "baseline" }}><SvgIcon iconClass="folder" fontSize="12" /><span className="pi-mg-left-2">{text}</span></div>
+        } else {
+        return <div><span style={{ color: '#6c757d', marginRight: '.5rem' }}>{record.isEnd ? '└' : '├'}</span>
+        <Link to={{ pathname: '/console/contract/content/summary', state: { id: record.bidsectionId } }}>{text}</Link>
+        </div>
+        }
+      }
+    },
+    {
+      title: '合同总数',
+      dataIndex: 'contracts',
+      key: 'contracts',
+      width: '12%',
+      align: 'right'
+    },
+    {
+      title: '收入合同金额',
+      dataIndex: 'contractsIncome',
+      key: 'contractsIncome',
+      width: '18%',
+      align: 'right'
+    },
+    {
+      title: '回款进度',
+      dataIndex: 'contractsIncomeProgress',
+      key: 'contractsIncomeProgress',
+      width: '12%',
+      align: 'center'
+    },
+    {
+      title: '支出合同金额',
+      dataIndex: 'contractsPay',
+      key: 'contractsPay',
+      width: '18%',
+      align: 'right'
+    },
+    {
+      title: '支付进度',
+      dataIndex: 'contractsPayProgress',
+      key: 'contractsPayProgress',
+      width: '12%',
+      align: 'center'
+    }
+  ]
+  const handleMenuClick = ({ key }: any) => {
+    message.info(`Click on item ${key}`)
+  }
+  const menu = (
+    <Menu onClick={handleMenuClick}>
+      <Menu.Item key="1">展开所有</Menu.Item>
+      <Menu.Item key="2">收起所有</Menu.Item>
+    </Menu>
+  )
+
+  useActivate(() => {
+    getTree()
+  })
+
+  useEffect(() => {
+    getTree()
+  }, [])
+  return (
+    <div className="list-content">
+      <Header>
+        <Slot>
+          <Dropdown overlay={menu}>
+            <Button type="text" size="small" className={styles.textBtn}>展开/收起<CaretDownOutlined /></Button>
+          </Dropdown>
+        </Slot>
+      </Header>
+      <div className={styles.tableContent}>
+        <Table<ContractTree>
+          columns={columns}
+          dataSource={tree.children}
+          pagination={false}
+          rowKey={record => record.id}
+          indentSize={20}
+          bordered
+        >
+        </Table>
+      </div>
+    </div>
+  )
+}
+export default List

+ 1 - 5
src/pages/Contract/index.tsx

@@ -1,4 +1,3 @@
-import LeftSide from '@/components/LeftSide'
 import Guards from '@/components/Navigation'
 import { NavigationGuardsProps } from '@/types/router'
 import React from 'react'
@@ -7,12 +6,9 @@ const Contract:React.FC<NavigationGuardsProps> = props => {
   const { routeConfig, match, location } = props
   return (
     <>
-      <LeftSide childRoutes={routeConfig} location={location}></LeftSide>
-      <div className="panel-content">
-        <Switch>
+      <Switch>
             <Guards routeConfig={routeConfig} match={match} location={location}></Guards>
         </Switch>
-      </div>
     </>
   )
 }

+ 1 - 0
src/pages/Management/Tender/components/ModalForm.tsx

@@ -134,6 +134,7 @@ const ModalForm: React.FC<iModalFormProps> = ({
 
 function mapTree(treeArr:TenderTree[], id: string, isFolder: boolean) {
   const arr =  treeArr?.map((tree:TenderTree) => {
+    if (!tree.isfolder) return
     const newTree: Option = { value: tree.id, title: tree.name, disabled: tree.id === id }
     // 要移动的是文件夹
     if (isFolder && tree.isBid) {

+ 33 - 20
src/router/routes.ts

@@ -40,29 +40,42 @@ export const routeConfig: RouteModel[] = [
         },
         childRoutes: [
           {
-            path: 'summary',
-            component: AsyncModuleLoader(() => import('@/pages/Contract/Summary')),
-            auth: [ 'USER' ],
+            path: 'list',
+            component: AsyncModuleLoader(() => import('@/pages/Contract/List')),
             defaultChildRoute: true,
-            meta: {
-              title: '合同概况'
-            }
+            auth: [ 'USER', 'ADMIN' ]
           },
           {
-            path: 'income',
-            component: AsyncModuleLoader(() => import('@/pages/Contract/Income')),
-            auth: [ 'USER' ],
-            meta: {
-              title: '收入合同'
-            }
-          },
-          {
-            path: 'spending',
-            component: AsyncModuleLoader(() => import('@/pages/Contract/Spending')),
-            auth: [ 'USER' ],
-            meta: {
-              title: '支出合同'
-            }
+            path: 'content',
+            component: AsyncModuleLoader(() => import('@/pages/Contract/Content')),
+            auth: [ 'USER', 'ADMIN' ],
+            childRoutes: [
+              {
+                path: 'summary',
+                component: AsyncModuleLoader(() => import('@/pages/Contract/Content/Summary')),
+                defaultChildRoute: true,
+                auth: [ 'USER', 'ADMIN' ],
+                meta: {
+                  title: '合同概况'
+                }
+              },
+              {
+                path: 'income',
+                component: AsyncModuleLoader(() => import('@/pages/Contract/Content/Income')),
+                auth: [ 'USER', 'ADMIN' ],
+                meta: {
+                  title: '收入合同'
+                }
+              },
+              {
+                path: 'spending',
+                component: AsyncModuleLoader(() => import('@/pages/Contract/Content/Spending')),
+                auth: [ 'USER', 'ADMIN' ],
+                meta: {
+                  title: '支出合同'
+                }
+              }
+            ]
           }
         ]
       },

+ 3 - 1
src/store/mobx/index.ts

@@ -1,7 +1,9 @@
 import frameState from './frame'
+import tenderState from './tender'
 import userState from './user'
 export {
   userState,
-  frameState
+  frameState,
+  tenderState
 }
 

+ 11 - 0
src/store/mobx/tender/index.ts

@@ -0,0 +1,11 @@
+import { action, observable } from "mobx"
+
+class Tender {
+  @observable bidsectionId:string = ''
+
+  @action saveBidsectionId(id: string) {
+    this.bidsectionId = id
+  }
+}
+
+export default new Tender()

+ 31 - 0
src/types/contract.d.ts

@@ -0,0 +1,31 @@
+export interface ContractTree {
+  bidsectionId: string;
+  children: ContractTree[] | null[];
+  childsTotal: number;
+  contracts: number;
+  contractsIncome: string;
+  contractsIncomeProgress: string;
+  contractsPaid: string;
+  contractsPay: string;
+  contractsPayProgress: string;
+  contractsReturned: string;
+  csrf: string;
+  hasFolder: boolean;
+  id: string;
+  isBid: boolean;
+  isEnd: boolean;
+  isfolder: number;
+  name: string;
+  parentId: string;
+  projectId: string;
+}
+export interface iModalBooleanProps {
+  type : string
+  visible: boolean
+  confirmLoading: boolean
+}
+export interface iModalCommonProps {
+  modalObj: iModalBooleanProps
+  onConfirm: (values: any, type: string) => void
+  onCancel: () => void
+}

+ 1 - 0
src/utils/util.ts

@@ -77,6 +77,7 @@ const combinationPath = (parentPath: string|undefined, pathOfTargetConfig: strin
   return combinedPath
 }
 
+
 export {
   getCookie,
   storage,