Browse Source

feat: 增加支出合同

lanjianrong 4 years ago
parent
commit
da02a52d61

+ 2 - 1
package.json

@@ -6,7 +6,8 @@
   "scripts": {
     "dev": "node scripts/start.js",
     "build": "node scripts/build.js",
-    "test": "node scripts/test.js"
+    "test": "node scripts/test.js",
+    "deploy": "auto-deploy build -t win"
   },
   "browserslist": {
     "production": [

+ 103 - 0
src/pages/Contract/Content/Spending/api.ts

@@ -0,0 +1,103 @@
+import request from '@/utils/common/request'
+
+/**
+ * 标段升降、上下移、增删
+ * @param type - 操作类型
+ * @param payload - 载荷
+ */
+export async function apiResfulContractTree(type: string, payload: object) {
+  let url: string = '', method: string = ''
+  switch (type) {
+    case 'serial':
+      url = '/api/contract/section/serial'
+      method = 'post'
+      break
+    case 'depth':
+      url = '/api/contract/section/depth'
+      method = 'post'
+      break
+    case 'add':
+      url = '/api/contract/section/add'
+      method = 'post'
+      break
+    case 'del':
+      url = '/api/contract/section'
+      method = 'del'
+      break
+    default:
+      break
+  }
+  const { data } = await request[method](url, payload)
+  return data
+}
+
+/**
+ * 获取单个合同详情和项目节详情
+ * @param id - 项目节id
+ * @param bidsectionId - 标段id
+ */
+export async function apiGetIncome(id: string, bidsectionId: string) {
+  const { data } = await request.get('/api/contract/income', { id, bidsectionId })
+  return data
+}
+
+/**
+ * 修改合同节序号
+ * @param id 项目节id
+ * @param bidsectionId 标段id
+ * @param serial 序号
+ */
+export async function apiUpdateSerial(id: string, bidsectionId: string, serial: string) {
+  const { data } = await request.post('/api/contract/section/serial/update', { id, bidsectionId, serial: parseInt(serial) })
+  return data
+}
+
+/**
+ * 修改合同节名称
+ * @param id 项目节id
+ * @param bidsectionId 标段id
+ * @param name 名称
+ */
+export async function apiUpdateName(id: string, bidsectionId: string, name: string) {
+  const { data } = await request.post('/api/contract/section/save', { id, bidsectionId, name })
+  return data
+}
+
+/**
+ * 合同增删改(包括添加回款)
+ * @param type - 操作类型
+ * @param payload - 载荷
+ */
+export async function apiResfulContract(type: string, payload: object) {
+  let url: string = '', method: string = ''
+  switch (type) {
+    case 'create':
+      url = '/api/contract/income/create'
+      method = 'post'
+      break
+    case 'update':
+      url = '/api/contract/income/update'
+      method = 'post'
+      break
+    case 'close':
+      url = '/api/contract/close'
+      method = 'post'
+      break
+    case 'del':
+      url = '/api/contract'
+      method = 'del'
+      break
+    case 'unlock':
+      url = '/api/contract/unlock'
+      method = 'post'
+      break
+    case 'return':
+      url = '/api/contract/return/create'
+      method = 'post'
+      break
+    default:
+      break
+  }
+  const { data } = await request[method](url, payload)
+  return data
+}

+ 11 - 0
src/pages/Contract/Content/Spending/components/Modal/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/section/all', { 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
+}

+ 13 - 0
src/pages/Contract/Content/Spending/components/Modal/index.module.scss

@@ -0,0 +1,13 @@
+.contractFormItem {
+  :global(.ant-input-group-addon) {
+    padding: 0;
+    &:hover {
+      color: #ffffff;
+      background-color: #6c757d;
+      border-color: #6c757d;
+    }
+    &:not(:disabled) {
+      cursor: pointer;
+    }
+  }
+}

+ 247 - 0
src/pages/Contract/Content/Spending/components/Modal/index.tsx

@@ -0,0 +1,247 @@
+import DatePicker from '@/components/DatePicker'
+import { apiAutoCode } from '@/pages/Safe/Content/List/api'
+import { contractStore, tenderStore } from '@/store/mobx'
+import { iModalCommonProps } from '@/types/contract'
+import consts from '@/utils/consts'
+import { dayjsFormat } from '@/utils/util'
+import { Button, Form, Input, Modal, Select } from 'antd'
+import locale from 'antd/es/date-picker/locale/zh_CN'
+import React, { useEffect, useState } from 'react'
+import { apiGetReturnWay } from '../Tabs/Receivable/api'
+import styles from './index.module.scss'
+const ContractModal: React.FC<iModalCommonProps> = ({ modalObj: { type, visible, confirmLoading }, onConfirm, onCancel, reload, row }) => {
+  const { Option } = Select
+  const [ form ] = Form.useForm()
+  const [ options, setOptions ] = useState<string[]>([])
+  const modalObj = {
+    create: {
+      title: '新建合同',
+      cancelText: '取消',
+      okText: '确认添加'
+    },
+    update: {
+      title: '编辑合同',
+      cancelText: '取消',
+      okText: '确认'
+    },
+    close: {
+      title: '关闭合同',
+      cancelText: '取消',
+      okText: '确认关闭'
+    },
+    del: {
+      title: '删除合同',
+      cancelText: '取消',
+      okText: '确认删除'
+    },
+    unlock: {
+      title: '解锁合同',
+      cancelText: '取消',
+      okText: '确认解锁'
+    },
+    return: {
+      title: '添加回款',
+      cancelText: '关闭',
+      okText: '确认'
+    }
+  }
+
+
+  useEffect(() => {
+    if (visible) {
+      form.setFieldsValue({ treeId: row.id, bidsectionId: row.bidsectionId })
+      if (type === "update") {
+        const { content="", name="", price="", partyA="", partyB="", partyASigner="",partyBSigner="" } = contractStore.contract
+        form.setFieldsValue({ content, name, price, partyA, partyB, partyASigner, partyBSigner })
+      } else if (type === 'return') {
+        apiGetReturnWay().then(({ code = -1, data = [] }) => {
+          if (code === consts.RET_CODE.SUCCESS) {
+            const options = data.map((item: string) => <Option key={item} value={item}>{item}</Option>)
+            setOptions(options)
+          }
+        })
+        form.setFieldsValue({ contractsId: contractStore.contract.id })
+      } else {
+        form.setFieldsValue({ id: contractStore.contract.id })
+      }
+    }
+  }, [ visible ])
+  const autoCodeHandler = async () => {
+    const { code = -1, data = "" } = await apiAutoCode(tenderStore.tender.bidsectionId, 'contractRule')
+    if (code === consts.RET_CODE.SUCCESS) {
+      if (data) {
+        const ruleArr: string[] = []
+        const code = JSON.parse(data)
+        for (const key in code) {
+          if (Object.prototype.hasOwnProperty.call(code, key)) {
+            const element = code[key]
+            if (element) {
+              ruleArr.push(element)
+            }
+          }
+        }
+        form.setFieldsValue({ code: ruleArr.join("-") })
+      }
+    }
+  }
+  return (
+    <Modal
+      getContainer={false}
+      visible={visible}
+      title={modalObj[type]?.title}
+      onCancel={onCancel}
+      footer={<div className="pi-justify-end">
+        {type === 'update' ? <Button type="primary" key="delete" size="small" danger onClick={() => reload('del')}>删除合同</Button> : ''}
+        <Button type="default" size="small" key="cancel" className="pi-btn-secondary" onClick={onCancel}>{modalObj[type]?.cancelText}</Button>
+        <Button
+          type="primary"
+          size="small"
+          key="ok"
+          loading={confirmLoading}
+          danger={type === 'del'}
+          onClick={() => {
+            form.validateFields().then(values => {
+              form.resetFields()
+              if (type === 'update') {
+                values.signerTime = dayjsFormat(values.signerTime, 'YYYY-MM-DD HH:mm:ss')
+              }
+              if (type === 'del') {
+                delete values.warningText
+              }
+              if (type === 'return') {
+                values.time = dayjsFormat(values.time, 'YYYY-MM-DD HH:mm:ss')
+              }
+              onConfirm(values, type)
+            }).catch(info => {
+              console.error('Validate Failed:', info)
+            })
+          }}>{modalObj[type]?.okText}</Button>
+      </div>}
+    >
+      <Form
+        form={form}
+        layout='vertical'
+        style={type === 'update' ? { maxHeight: '482px', overflowY: 'scroll', paddingRight: 5 } : { overflow: 'hidden' }}
+        >
+        <Form.Item name="bidsectionId" hidden>
+          <Input></Input>
+        </Form.Item>
+        <Form.Item name="treeId" label="合同劳务" hidden>
+          <Input></Input>
+        </Form.Item>
+        {
+          type === 'create' ? (
+          <>
+            <Form.Item name="code" label="合同编号" rules={[ { required: true, message: '请输入合同编号' } ]} className={styles.contractFormItem}>
+              <Input addonAfter={<span className="pi-pd-lr-11" onClick={() => autoCodeHandler()}>自动编号</span>}></Input>
+            </Form.Item>
+            <Form.Item name="name" label="合同名称" rules={[ { required: true, message: '请输入合同名称' } ]}>
+              <Input placeholder="输入合同名称"></Input>
+            </Form.Item>
+            <Form.Item name="contractsType" label="合同类型" rules={[ { required: true, message: '请选择合同类型' } ]}>
+              <Select showSearch>
+                <Option value={1}>支出合同</Option>
+                <Option value={2}>收入合同</Option>
+              </Select>
+            </Form.Item>
+            <Form.Item name="price" label="合同金额" rules={[ { required: true, message: '请输入合同金额' } ]}>
+              <Input placeholder="输入合同金额" addonAfter={<span>元</span>}></Input>
+            </Form.Item>
+          </>
+          ) : ''
+        }
+        {
+          type === 'update' ? (
+            <>
+              <Form.Item name="content" label="项目内容" rules={[ { required: true, message: '请输入项目内容' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="name" label="合同名称" rules={[ { required: true, message: '请输入项目内容' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="price" label="合同金额" rules={[ { required: true, message: '请输入项目金额' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="partyA" label="甲方" rules={[ { required: true, message: '请输入甲方' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="partyASigner" label="甲方签约人" rules={[ { required: true, message: '请输入甲方签约人' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="partyB" label="乙方" rules={[ { required: true, message: '请输入乙方' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="partyBSigner" label="乙方签约人" rules={[ { required: true, message: '请输入乙方签约人' } ]}>
+                <Input></Input>
+              </Form.Item>
+              <Form.Item name="signerTime" label="合同签约日期" rules={[ { required: true, message: '请选择签约日期' } ]}>
+                <DatePicker allowClear locale={locale} className="pi-width-100P"></DatePicker>
+              </Form.Item>
+              <Form.Item name="remarks" label="备注">
+                <Input.TextArea maxLength={100}></Input.TextArea >
+              </Form.Item>
+            </>
+          ) : ''
+        }
+        {
+          type === 'close' ? (
+            <>
+              <Form.Item name="id" hidden><Input></Input></Form.Item>
+              <span>关闭后,合同将锁定,无法进行编辑、上传文件等操作。</span>
+            </>
+          ) : ''
+        }
+        {
+          type === 'unlock' ? (
+            <>
+              <Form.Item name="id" hidden><Input></Input></Form.Item>
+              <span>解锁后,合同将锁定,无法进行编辑、上传文件等操作。</span>
+            </>
+          ) : ''
+        }
+        {
+          type === "del" ? (
+            <>
+              <Form.Item name="id" hidden><Input></Input></Form.Item>
+              <p className="mb-2">删除后,数据无法恢复,请谨慎操作。</p>
+              <p className="mb-2">请在下方文本框输入文本「<span className="pi-red">确认删除本合同</span>」,以此确认删除操作。</p>
+              <Form.Item name="warningText"rules={[ () => ({
+                validator(rule, value) {
+                  if (!value || value !== "确认删除本合同") {
+                    return Promise.reject("请按照提示信息进行删除操作!")
+                  }
+                  return Promise.resolve()
+                }
+              }) ]}>
+                <Input placeholder="输入文本, 确认删除"></Input>
+              </Form.Item>
+            </>
+          ) : ''
+        }
+        {
+          type === 'return' ?
+          <>
+            <Form.Item name="contractsId" hidden><Input></Input></Form.Item>
+            <Form.Item name="time" label="回款日期" rules={[ { required: true, message: '请选择回款日期' } ]}>
+              <DatePicker allowClear locale={locale} className="pi-width-100P"></DatePicker>
+            </Form.Item>
+            <Form.Item name="price" label="回款金额" rules={[ { required: true, message: '请选择回款金额' } ]}>
+              <Input></Input>
+            </Form.Item>
+            <Form.Item name="way" label="支付方式" rules={[ { required: true, message: '请选择回款方式' } ]}>
+              <Select>
+                {options}
+              </Select>
+            </Form.Item>
+            <Form.Item name="remarks" label="备注">
+                <Input.TextArea maxLength={100}></Input.TextArea >
+            </Form.Item>
+          </>
+          : ''
+        }
+      </Form>
+    </Modal>
+  )
+}
+
+export default ContractModal

+ 63 - 0
src/pages/Contract/Content/Spending/components/TableContent/index.module.scss

@@ -0,0 +1,63 @@
+.projectTable {
+  :global(.ant-table-cell) {
+    padding: 0.3rem;
+  }
+}
+
+.spreadContent {
+  position: relative;
+  height: calc(100vh - 34px);
+  overflow: hidden;
+  .spreadSheets {
+    width: 100%;
+    height: 62%;
+    overflow: hidden;
+    :global(.ant-input-group-addon) {
+      padding: 0 6px;
+    }
+    // :global(.ant-table-tbody > tr > td) {
+    //   padding: 0.3rem;
+    // }
+    // :global(.ant-table-thead > tr > th) {
+    //   padding: 0.3rem;
+    //   color: #000000;
+    //   text-align: center;
+    //   background: #e9ecef;
+    //   border-bottom: 2px solid #dee2e6;
+    //   border-bottom-width: 2px;
+    // }
+  }
+  .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%;
+  }
+}

+ 419 - 0
src/pages/Contract/Content/Spending/components/TableContent/index.tsx

@@ -0,0 +1,419 @@
+import { ZhSubmitButton } from '@/components/Button'
+import OssUploadModal from '@/components/OssUpload'
+import { contractStore, tenderStore } from '@/store/mobx'
+import { iIncomeTree } from '@/types/contract'
+import { iFile } from '@/types/file'
+import { apiSaveFileInfo } from '@/utils/common/api'
+import { contractConsts } from '@/utils/common/constStatus'
+import consts from '@/utils/consts'
+import { Button, Input, message, Radio, Table, Tabs } from 'antd'
+import Modal from 'antd/lib/modal/Modal'
+import { RadioChangeEvent } from 'antd/lib/radio'
+import { ColumnsType } from 'antd/lib/table'
+import { observer } from 'mobx-react'
+import React, { KeyboardEvent, useEffect, useRef, useState } from 'react'
+import { apiGetIncome, apiResfulContractTree, apiUpdateName, apiUpdateSerial } from '../../api'
+import { apiContractIncome, apiSetTemplate } from '../Modal/api'
+import Detail from '../Tabs/Detail'
+import File from '../Tabs/File'
+import Receivable from '../Tabs/Receivable'
+import styles from './index.module.scss'
+interface iTableContentPorps {
+  modalHandler: (type: string) => void
+  row: iIncomeTree
+  setRow: (record: iIncomeTree) => 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<iTableContentPorps> = ({ modalHandler, row, setRow }) => {
+  const [ sectionTemplate, setSectionTemplate ] = useState<iShowTemplateState>({
+    isShow: false,
+    template: '',
+    loading: false
+  })
+  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: ''
+    }
+  })
+
+  // 阿里oss弹窗控制器
+  const [ visible, setVisible ] = useState<boolean>(false)
+
+  const { TabPane } = Tabs
+
+  useEffect(() => {
+    initHandler()
+  }, [])
+  const initHandler = async () => {
+    const data  = await apiContractIncome(tenderStore.bid)
+    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 {
+        contractStore.updateTree(data.sectionTree.children)
+      }
+    }
+  }
+  interface iLabelHandlerProps {
+    id: string
+    bidsectionId: string
+    name?: string
+  }
+
+  const newLabelHandler = async (type: string, payload: iLabelHandlerProps) => {
+    let RET_CODE: number = -1
+    if (type === 'create') {
+      payload.name = inputEl.current?.state.value
+      const { code = -1 } = await apiResfulContractTree('add', payload)
+      RET_CODE = code
+    }
+    if (type === 'edit') {
+      const name = inputEl.current?.state.value
+      const { code = -1 } = await apiUpdateName(payload.id, payload.bidsectionId, name)
+      RET_CODE = code
+    }
+    if (RET_CODE === consts.RET_CODE.SUCCESS) {
+      contractStore.resetTree(tenderStore.tender.bidsectionId)
+    }
+  }
+
+  const codeChange = async (row: iIncomeTree, value: string) => {
+    const { code = -1 } = await apiUpdateSerial(row.id, row.bidsectionId, value)
+    if (code === consts.RET_CODE.SUCCESS) {
+      initHandler()
+    }
+  }
+  const inputEl = useRef<Input>(null)
+  const modalColumns: ColumnsType<iTemplateState> = [
+    {
+      title: '项目节',
+      dataIndex: 'serial',
+      width: '30%',
+      // 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: '70%'
+    }
+  ]
+
+  const TableColumns: ColumnsType<iIncomeTree> = [
+    {
+      title: '编号',
+      dataIndex: 'code',
+      width: '15%',
+      // eslint-disable-next-line react/display-name
+      render: (text: string, row: iIncomeTree) => {
+        if (row.isEdit) {
+          return (
+          <Input
+            defaultValue={row.serial}
+            addonBefore={row.attribution}
+            size="small"
+            style={{ width: 80 }}
+            onPressEnter={(e: KeyboardEvent<HTMLInputElement>) => codeChange(row, e.currentTarget.value)}
+            />)
+        } else {
+          return <span>{row.code}</span>
+        }
+        // return <span>{row.code}</span>
+      }
+    },
+    {
+      title: '项目名称',
+      dataIndex: 'name',
+      render: (text:any, record: iIncomeTree) => {
+        if (record.isEdit || record.isNew) {
+          const type = record.isEdit ? 'edit' : 'create'
+          return (
+          <Input
+            defaultValue={record.name}
+            size="small"
+            type="text"
+            ref={inputEl}
+            onPressEnter={() => newLabelHandler(type, { id: type === 'edit' ? record.id : record.parentId, bidsectionId: record.bidsectionId })}
+            onBlur={() => newLabelHandler(type, { id: type === 'edit' ? record.id : record.parentId, bidsectionId: record.bidsectionId })}
+            />)
+        } else {
+          return <span>{text}</span>
+        }
+      }
+    },
+    {
+      title: '合同名称',
+      dataIndex: 'contractName'
+    },
+    {
+      title: '合同编号',
+      dataIndex: 'contractCode'
+    },
+    {
+      title: '合同金额',
+      dataIndex: 'contractPrice',
+      align: 'right',
+      // eslint-disable-next-line react/display-name
+      render: (text:any, record: iIncomeTree) => record.contractCode ? <span>{text}</span> : ''
+    },
+    {
+      title: '回款金额',
+      dataIndex: 'contractReturned',
+      align: 'right',
+      // eslint-disable-next-line react/display-name
+      render: (text:any, record: iIncomeTree) => record.contractCode ? <span>{text}</span> : ''
+    },
+    {
+      title: '状态',
+      dataIndex: 'contractStatus',
+      // eslint-disable-next-line react/display-name
+      render: (_: any, record: iIncomeTree) => record.contractCode ? <span><i className={contractConsts[record.contractStatus].className}></i>{contractConsts[record.contractStatus].text}</span> : ''
+    }
+
+  ]
+
+  // modal 确认 - 回调
+  const handleModalConfirm = async () => {
+    setSectionTemplate({
+      ...sectionTemplate,
+      loading: true
+    })
+    if (!sectionTemplate.template) {
+      setSectionTemplate({
+        ...sectionTemplate,
+        loading: false
+      })
+      return message.error('请选择项目节模板!')
+    }
+    const { code = -1 } = await apiSetTemplate(sectionTemplate.template, tenderStore.tender.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
+      })
+    }
+  }
+
+  const onClickRow  = (record: iIncomeTree) => {
+    return {
+      onClick() {
+        rowClickHandler(record.id, record.bidsectionId, record.isEdit, record.isNew)
+      },
+      onDoubleClick() {
+        contractStore.rowChange(row.id)
+      }
+    }
+  }
+
+  // 行点击回调
+  const rowClickHandler = async (id: string, bid: string, isEdit?: boolean, isNew?: boolean) => {
+    if (!isEdit && !isNew) {
+      const { code = -1, section = {}, contract: newContract = {} } = await apiGetIncome(id, bid)
+      if (code === consts.RET_CODE.SUCCESS) {
+        setRow(section)
+        contractStore.updateContract(newContract)
+      }
+    }
+  }
+  const handleRowClass = (record: iIncomeTree) => {
+    return record.id === row.id ? 'ant-table-row-selected' : ''
+  }
+  const tabOnClick = (key: string) => {
+    contractStore.changeUpdate(key)
+  }
+
+  // 阿里oss上传弹窗
+  const onShow = (show: boolean) => setVisible(show)
+  const onCreate = async (fileList: iFile[]) => {
+    const { code = -1 } = await apiSaveFileInfo(fileList, consts.DATA_TYPE.CONTRACT, row.contractId)
+    if (code === consts.RET_CODE.SUCCESS) {
+      setVisible(false)
+      contractStore.changeUpdate('3')
+    }
+  }
+
+  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">项目节模板1</span></Radio>
+        </div>
+        <div className={styles.projectTable}>
+          {
+            tempalte.template1?.children &&  tempalte.template1?.children.length?
+            <Table
+            dataSource={tempalte.template1?.children}
+            columns={modalColumns}
+            pagination={false}
+            bordered
+            scroll={{ y: '300px' }}
+            rowKey={record => record.id}
+            defaultExpandAllRows={true}
+            >
+          </Table> : ''
+          }
+        </div>
+
+      </div>
+      <div className={styles.rightTemplate}>
+        <div className="pi-pd-20 pi-gray">
+          <Radio value="2" checked={sectionTemplate.template === '2'} onChange={(e: RadioChangeEvent) => handleRadioEvent(e)}><span className="pi-gray">项目节模板2</span></Radio>
+        </div>
+        <div className={styles.projectTable}>
+          {
+            tempalte.template2?.children &&  tempalte.template2?.children.length?
+            <Table
+            dataSource={tempalte.template2?.children}
+            columns={modalColumns}
+            bordered
+            pagination={false}
+            scroll={{ y: '300px' }}
+            rowKey={record => record.id}
+            defaultExpandAllRows={true}
+            />
+            : ''
+          }
+
+        </div>
+      </div>
+    </div>
+  </Modal>
+  :
+  <div className={styles.spreadContent}>
+    <div className={styles.spreadSheets}>
+      {
+        contractStore.showTable ?
+          <Table<iIncomeTree>
+            dataSource={contractStore.tree}
+            columns={TableColumns}
+            bordered
+            pagination={false}
+            rowKey={record => record.id}
+            defaultExpandAllRows={true}
+            onRow={onClickRow}
+            rowClassName={handleRowClass}
+            style={{ height: '100%', overflowY: 'scroll' }}
+          />
+          : ''
+        }
+    </div>
+    <div className={styles.extraControl}>
+      <Tabs
+        type="card"
+        size="small"
+        defaultActiveKey="1"
+        onTabClick={(key: string) => tabOnClick(key)}
+        tabBarExtraContent={{ right:
+          <div className="pi-mg-right-5 pi-flex-row">
+            {
+              contractStore.contract.id && contractStore.contract.status === contractConsts.status.checking ?
+              <>
+                <Button type="primary" size="small" onClick={() => modalHandler('update')} className="pi-mg-right-5">编辑合同</Button>
+                <Button type="primary" size="small" onClick={() => modalHandler('return')} className="pi-mg-right-5">添加回款</Button>
+                <Button type="primary" size="small" onClick={() => setVisible(true)}>上传文件</Button>
+              </>
+              : ''
+            }
+            {
+              contractStore.contract.id && contractStore.contract.status === contractConsts.status.willClose ?
+              <Button type="primary" size="small" danger className="pi-mg-right-3" onClick={() => modalHandler('close')}>关闭合同</Button>
+              : ''
+            }
+            {
+              contractStore.contract.id && contractStore.contract.status === contractConsts.status.closed ?
+              <ZhSubmitButton type="primary" size="small" danger className="pi-mg-right-3" onClick={() => modalHandler('unlock')}>解锁合同</ZhSubmitButton>
+              : ''
+            }
+          </div>
+        }}>
+        <TabPane key="1" tab="合同详情">
+          <Detail {...contractStore.contract}></Detail>
+        </TabPane>
+        <TabPane key="2" tab="合同回款">
+          <Receivable></Receivable>
+        </TabPane>
+        <TabPane key="3" tab="合同文件">
+          <File></File>
+        </TabPane>
+      </Tabs>
+    </div>
+    <OssUploadModal
+      visible={visible}
+      onCancel={() => setVisible(false)}
+      onCreate={onCreate}
+      onShow={onShow}
+    ></OssUploadModal>
+  </div>
+}
+
+export default observer(GCsheet)

+ 41 - 0
src/pages/Contract/Content/Spending/components/Tabs/Detail/index.module.scss

@@ -0,0 +1,41 @@
+.detailTab {
+  flex: 0 0 66.666667%;
+  max-width: 66.666667%;
+  padding: 0 15px;
+  .detailTable {
+    margin-bottom: 1rem;
+    color: #212529;
+    border: 1px solid #dee2e6;
+    & > tbody tr th {
+      font-weight: normal;
+      color: #000000;
+      background: #e9ecef;
+      border: 1px solid #dee2e6;
+    }
+    & > tbody tr th,
+    & > tbody tr td {
+      padding: 0.3rem;
+      border: 1px solid #dee2e6;
+    }
+    .progressContainer {
+      padding: 0.3rem;
+      .progressContent {
+        display: flex;
+        height: 1rem;
+        overflow: hidden;
+        font-size: 0.75rem;
+        background-color: #e9ecef;
+        border-radius: 0.25rem;
+        .progressBar {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          color: #ffffff;
+          text-align: center;
+          white-space: nowrap;
+          transition: width 0.6s ease;
+        }
+      }
+    }
+  }
+}

+ 27 - 0
src/pages/Contract/Content/Spending/components/Tabs/Detail/index.tsx

@@ -0,0 +1,27 @@
+import { iContractState } from '@/types/contract'
+import { contractConsts } from '@/utils/common/constStatus'
+import { dayjsFormat } from '@/utils/util'
+import React from 'react'
+import styles from './index.module.scss'
+export default function Detail(props: iContractState) {
+  return (
+    <div className={styles.detailTab}>
+      <table className={styles.detailTable}>
+        <tbody>
+        <tr><th style={{ width: '10%' }}>项目内容</th><td style={{ width: '40%' }}>{props.content}</td><th style={{ width: '10%' }}>合同名称</th><td style={{ width: '40%' }}>{props.name}</td></tr>
+        <tr><th>合同编号</th><td>{props.code}</td><th>状态</th><td><i className={contractConsts[props.status].class}></i>{contractConsts[props.status].text}</td></tr>
+        <tr><th>合同金额</th><td>{props.price}</td><th>创建时间</th><td>{props.createTime}</td></tr>
+        <tr><th>回款金额</th><td>{props.paid}</td><th>未回款金额</th><td>2,769,700.00</td></tr>
+          <tr><th>回款进度</th><td className={styles.progressContainer} colSpan={3}><div className={styles.progressContent}>
+            <div className={[ styles.progressBar, 'pi-bg-success' ].join(' ')} style={{ width: '57.7%' }} >57.7%</div>
+            <div className={[ styles.progressBar, 'pi-bg-gray' ].join(' ')} style={{ width: '42.3%' }}>42.3%</div>
+          </div></td></tr>
+        <tr><th>甲方</th><td>{props.partyA}</td><th>甲方签约人</th><td>{props.partyASigner}</td></tr>
+        <tr><th>乙方</th><td>{props.partyB}</td><th>乙方签约人</th><td>{props.partyBSigner}</td></tr>
+        <tr><th>签约日期</th><td colSpan={3}>{props.signerTime && dayjsFormat(props.signerTime, 'YYYY-MM-DD')}</td></tr>
+        <tr><th>备注</th><td colSpan={3}>{props.remarks}</td></tr>
+        </tbody>
+      </table>
+    </div>
+  )
+}

+ 0 - 0
src/pages/Contract/Content/Spending/components/Tabs/File/index.module.scss


+ 102 - 0
src/pages/Contract/Content/Spending/components/Tabs/File/index.tsx

@@ -0,0 +1,102 @@
+import { contractStore } from '@/store/mobx'
+import { apiDelFile, apiGetFileList } from '@/utils/common/api'
+import consts from '@/utils/consts'
+import { dayjsFormat } from '@/utils/util'
+import Table, { ColumnsType } from 'antd/lib/table'
+import { observer } from 'mobx-react'
+import React, { useEffect, useState } from 'react'
+
+interface iFileState {
+  id: string
+  filename: string
+  filepath: string
+  accountName: string
+  createTime: string
+}
+
+const File:React.FC<{}> = () => {
+  const [ data, setData ] = useState<Array<iFileState>>([])
+  const [ total, setTotal ] = useState<number>(0)
+  const [ id, setId ] = useState<string>('')
+  useEffect(() => {
+    if (contractStore.contract.id) {
+      if (contractStore.contract.id !== id) {
+        setId(contractStore.contract.id)
+        initData()
+      } else if (contractStore.shouldUpdate && contractStore.shouldUpdate === '3') {
+        initData()
+      }
+      contractStore.shouldUpdate && (contractStore.changeUpdate(''))
+    }
+  }, [ contractStore.contract.id, contractStore.shouldUpdate ])
+  const initData = async(pageNo: number = 1, pageSize: number = 7) => {
+    const { code = -1, data = [], total = 0 } = await apiGetFileList(consts.DATA_TYPE.CONTRACT, contractStore.contract.id, pageNo, pageSize)
+    if (code === consts.RET_CODE.SUCCESS) {
+      setData(data)
+      setTotal(total)
+    }
+  }
+  const deleteFile = async(id: string) => {
+    const { code = -1 }  = await apiDelFile(id)
+    if (code === consts.RET_CODE.SUCCESS) {
+      const newData = data.filter((file: iFileState) => file.id !== id)
+      setData(newData)
+    }
+  }
+  const columns: ColumnsType<iFileState> = [
+    {
+      dataIndex: 'sort',
+      // eslint-disable-next-line react/display-name
+      render: (_:any, record: iFileState, idx: number) => <span>{idx+1}</span>,
+      width: 5
+    },
+    {
+      title: '名称',
+      dataIndex: 'filename',
+      // eslint-disable-next-line react/display-name
+      render: (text: string, record: iFileState) => <a href={consts.OSS_PATH.REVIEW + record.filepath} target="_blank" rel="noopener noreferrer">{text}</a>,
+      width: 50
+    },
+    {
+      title: '上传者',
+      dataIndex: 'accountName',
+      width: 10
+    },
+    {
+      title: '上传时间',
+      dataIndex: 'createTime',
+      // eslint-disable-next-line react/display-name
+      render: (time: string) => <span>{dayjsFormat(time, 'YYYY-MM-DD')}</span>,
+      width: 20
+    },
+    {
+      title: '操作',
+      dataIndex: 'opreate',
+      // eslint-disable-next-line react/display-name
+      render:(_: string, record: iFileState) => {
+        return <div>
+        <a className="pi-mg-right-5" download href={consts.OSS_PATH.DOWNLOAD + record.filepath}>下载</a>
+        <span className="pi-link-red" onClick={() => deleteFile(record.id)}>删除</span>
+      </div>
+      },
+      width: 15
+    }
+  ]
+  return (
+    <Table
+      dataSource={data}
+      columns={columns}
+      bordered
+      rowKey={record => record.id}
+      pagination={{
+        hideOnSinglePage: true,
+        size: "small",
+        pageSize: 7,
+        onChange: (page, pageSize) => initData(page, pageSize),
+        total
+      }}
+      ></Table>
+  )
+}
+
+export default observer(File)

+ 39 - 0
src/pages/Contract/Content/Spending/components/Tabs/Receivable/api.ts

@@ -0,0 +1,39 @@
+import request from '@/utils/common/request'
+
+/**
+ * 删除回款
+ * @param id 回款id
+ * @param contractsId 合同id
+ * @param bidsectionId 标段id
+ */
+export async function apiDelReturn(id: string, contractsId: string, bidsectionId: string) {
+  const { data } = await request.del('/api/contract/return/delete', { id, contractsId, bidsectionId })
+  return data
+}
+
+/**
+ * 获取合同回款列表
+ * @param constractsId 合同id
+ * @param bidsectionId 标段id
+ */
+export async function apiGetReturns(contractsId: string, bidsectionId: string) {
+  const { data } = await request.get('/api/contract/return/list', { contractsId, bidsectionId, page: 1 })
+  return data
+}
+
+/**
+ * 获取回款类型
+ */
+export async function apiGetReturnWay() {
+  const { data } = await request.get('/api/contract/return/way')
+  return data
+}
+
+/**
+ * 更新回款内容
+ * @param payload 载荷
+ */
+export async function apiUpdateReturn(payload: object) {
+  const { data } = await request.post('/api/contract/return/update', payload)
+  return data
+}

+ 0 - 0
src/pages/Contract/Content/Spending/components/Tabs/Receivable/index.module.scss


+ 266 - 0
src/pages/Contract/Content/Spending/components/Tabs/Receivable/index.tsx

@@ -0,0 +1,266 @@
+import DatePicker from '@/components/DatePicker'
+import FileModal from '@/components/FileModal'
+import { contractStore } from '@/store/mobx'
+import { iFileModal } from '@/types/file'
+import consts from '@/utils/consts'
+import { dayjsFormat } from '@/utils/util'
+import { DisconnectOutlined } from '@ant-design/icons'
+import { Form, Input, Popconfirm, Table } from 'antd'
+import locale from 'antd/es/date-picker/locale/zh_CN'
+import dayjs from 'dayjs'
+import { observer } from 'mobx-react'
+import React, { useEffect, useMemo, useState } from 'react'
+import { apiDelReturn, apiGetReturns, apiUpdateReturn } from './api'
+
+interface iReceivableState {
+  accountId: string;
+  annexes: number;
+  bidsectionId: string;
+  contractsId: string;
+  createTime: string;
+  createUser: string;
+  fileCounts: number;
+  id: string;
+  page: number;
+  price: string;
+  projectId: string;
+  remarks: string;
+  time: string;
+  way: string;
+}
+interface EditableCellProps extends React.HTMLAttributes<HTMLElement> {
+  editing: boolean;
+  dataIndex: string;
+  title: any;
+  cellType: 'DatePicker' | 'text';
+  record: iReceivableState;
+  index: number;
+  children: React.ReactNode;
+}
+
+const EditableCell: React.FC<EditableCellProps> = ({
+  editing,
+  dataIndex,
+  title,
+  cellType,
+  children,
+  record,
+  index,
+  ...restProps
+}) => {
+  // console.log(dataIndex, record)
+
+  const cellNode = cellType === 'text' ? <Input size="small" allowClear/> : <DatePicker size="small" allowClear locale={locale} />
+
+  const isDate = useMemo(() => {
+    return dataIndex === 'createTime' || dataIndex === 'time'
+  }, [ dataIndex ])
+  return (
+    <td {...restProps}>
+      {editing ? (
+        <Form.Item name={dataIndex} initialValue={isDate ? dayjs(record[dataIndex]) : record[dataIndex]} style={{ margin: 0 }} rules={[ dataIndex === 'remarks' ? {} : { required: true, message: `请输入${title}!` } ]}>
+          {cellNode}
+        </Form.Item>
+      ) : (
+        children
+      )}
+    </td>
+  )
+}
+
+
+const Receivable:React.FC<{}> = () => {
+  const [ form ] = Form.useForm()
+  const [ data, setData ] = useState<Array<iReceivableState>>([])
+  const [ id, setId ] = useState<string>('')
+  const [ fileModal, setFileModal ] = useState<iFileModal>({
+    visible: false,
+    dataType: consts.DATA_TYPE.RETURN,
+    dataId: ''
+  })
+  const [ editingKey, setEditingKey ] = useState<string>('')
+
+  const delConfirm = async (id: string, contractsId: string, bidsectionId: string) => {
+    const { code = -1 } = await apiDelReturn(id, contractsId, bidsectionId)
+    if (code === consts.RET_CODE.SUCCESS) {
+      const newData = data.filter(item => item.id !== id)
+      setData(newData)
+    }
+  }
+
+  useEffect(() => {
+    if (contractStore.contract.id) {
+      if (contractStore.contract.id !== id) {
+        setId(contractStore.contract.id)
+        initData()
+      } else if (contractStore.shouldUpdate && contractStore.shouldUpdate === '2') {
+        initData()
+      }
+      contractStore.shouldUpdate && (contractStore.changeUpdate(''))
+    }
+  }, [ contractStore.contract.id, contractStore.shouldUpdate ])
+  const initData = async() => {
+    const { code = -1, data = [] } = await apiGetReturns(contractStore.contract.id, contractStore.contract.bidsectionId)
+      if (code === consts.RET_CODE.SUCCESS) {
+        setData(data)
+      }
+  }
+  const save = async (key: React.Key) => {
+    try {
+      const row = (await form.validateFields()) as iReceivableState
+
+      const newData = [ ...data ]
+      const index = newData.findIndex(item => key === item.id)
+      if (index > -1) {
+        const item = newData[index]
+        console.log(row)
+        console.log(item)
+        const payload = { ...row, time: dayjsFormat(row.time, 'YYYY-MM-DD'), createTime: dayjsFormat(row.createTime, 'YYYY-MM-DD'), id: item.id, bidsectionId: item.bidsectionId, contractsId: item.contractsId }
+        const { code = -1 } = await apiUpdateReturn(payload)
+        if (code === consts.RET_CODE.SUCCESS) {
+          newData.splice(index, 1, {
+            ...item,
+            ...row
+          })
+          setData(newData)
+        }
+      }
+      setEditingKey('')
+      // else {
+      //   newData.push(row)
+      //   setData(newData)
+      //   setEditingKey('')
+      // }
+    } catch (errInfo) {
+      console.log('Validate Failed:', errInfo)
+    }
+  }
+
+  const isEditing = (record: iReceivableState) => record.id === editingKey
+  const columns = [
+    {
+      dataIndex: 'sort',
+      width: '5%',
+      // eslint-disable-next-line react/display-name
+      render: (_: any, record: any, index: number) => {
+        return <span>{index + 1}</span>
+      }
+    },
+    {
+      title: '回款日期',
+      dataIndex: 'time',
+      editable: true,
+      width: '12%',
+      // eslint-disable-next-line react/display-name
+      render: (text: string) => <span>{dayjsFormat(text, 'YYYY-MM-DD')}</span>
+    },
+    {
+      title: '回款金额',
+      dataIndex: 'price',
+      editable: true,
+      width: '12%',
+      // eslint-disable-next-line react/display-name
+      render: (text: string) => <span className="pi-text-right pi-width-100P">{text}</span>
+    },
+    {
+      title: '回款方式',
+      dataIndex: 'way',
+      editable: true,
+      width: '12%'
+    },
+    {
+      title: '创建人',
+      dataIndex: 'createUser',
+      editable: true,
+      width: '12%'
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      editable: true,
+      width: '12%',
+      // eslint-disable-next-line react/display-name
+      render: (text: any) => <span>{dayjsFormat(text, 'YYYY-MM-DD')}</span>
+    },
+    {
+      title: '备注',
+      dataIndex: 'remarks',
+      editable: true,
+      width: '12%'
+    },
+    {
+      title: '附件',
+      dataIndex: 'fileCounts',
+      // eslint-disable-next-line react/display-name
+      render: (text: number, record: iReceivableState) => <span className="pi-pointer" onClick={() => setFileModal({ ...fileModal, dataId: record.id, visible: true })}><DisconnectOutlined /> {text}</span>
+    },
+    {
+      title: '操作',
+      dataIndex: 'opreate',
+      width: '10%',
+      // eslint-disable-next-line react/display-name
+      render: (text: any, record: iReceivableState) => {
+        const editable = isEditing(record)
+        return (
+        <div>
+          {
+            editable ?
+            (<><span className="pi-link-blue pi-mg-right-5" onClick={() => save(record.id)}>保存</span><span className="pi-link-blue" onClick={() => setEditingKey('')}>取消</span></>)
+            :
+            <span className="pi-link-blue" onClick={() => setEditingKey(record.id)}>编辑</span>
+          }
+          <Popconfirm title="确认删除?" cancelText="取消" okText="确认" onConfirm={() => delConfirm(record.id, record.contractsId, record.bidsectionId)}>
+            <span className="pi-link-red pi-mg-left-5">删除</span>
+          </Popconfirm>
+        </div>
+        )
+      }
+    }
+  ]
+
+  const cancel = () => {
+    setEditingKey('')
+  }
+
+
+  const mergedColumns = columns.map(col => {
+    if (!col.editable) {
+      return col
+    }
+    return {
+      ...col,
+      onCell: (record: iReceivableState) => ({
+        record,
+        cellType: col.dataIndex === 'createTime' || col.dataIndex === 'time' ? 'DatePicker' : 'text',
+        dataIndex: col.dataIndex,
+        title: col.title,
+        editing: isEditing(record)
+      })
+    }
+  })
+
+  return (
+    <>
+      <Form form={form} component={false}>
+        <Table
+          components={{ body: { cell: EditableCell } }}
+          dataSource={data}
+          columns={mergedColumns}
+          bordered
+          rowClassName="editable-row"
+          pagination={{ onChange: cancel, size:"small", pageSize: 7 }}
+          rowKey={record => record.id}
+          >
+        </Table>
+      </Form>
+      <FileModal
+        visible={fileModal.visible}
+        dataType={fileModal.dataType}
+        dataId={fileModal.dataId}
+        onCancel={() => setFileModal({ ...fileModal, visible: false })}
+        showUpload={true}
+      ></FileModal>
+    </>
+  )
+}
+export default observer(Receivable)

+ 186 - 6
src/pages/Contract/Content/Spending/index.tsx

@@ -1,12 +1,192 @@
-import { observer } from 'mobx-react'
-import React from 'react'
+import Header from '@/components/Header'
+import Slot from '@/components/Header/slot'
+import RuleModal from '@/components/RuleModal'
+import SvgIcon from '@/components/SvgIcon'
+import { apiSaveRule } from '@/pages/Safe/Content/List/api'
+import { contractStore, tenderStore } from '@/store/mobx'
+import { iIncomeTree, iModalBooleanProps } from '@/types/contract'
+import { contractTreeBaseId } from '@/utils/common/constStatus'
+import consts from '@/utils/consts'
+import { SettingOutlined } from '@ant-design/icons'
+import { Button, message, Tooltip } from 'antd'
+import React, { useMemo, useState } from 'react'
+import { apiResfulContract, apiResfulContractTree } from './api'
+import ContractModal from './components/Modal'
+import TableContent from './components/TableContent'
 
-const Spending:React.FC<{}> = () => {
+interface iModal {
+  visible: boolean
+  loading: boolean
+}
+
+export default function Income() {
+  const [ modalObj, setModalObj ] = useState<iModalBooleanProps>({
+    type: '',
+    visible: false,
+    confirmLoading: false
+  })
+  const [ row, setRow ] = useState<iIncomeTree>({
+    elderBrother: false,
+    isEnd: false,
+    attribsortution: 0,
+    attribution: '',
+    bidsectionId: '',
+    children: undefined,
+    code: '',
+    contractCode: '',
+    contractId: '',
+    contractName: '',
+    contractPrice: '',
+    contractReturned: '',
+    contractStatus: 0,
+    contractsPaid: '',
+    createTime: '',
+    depth: 0,
+    id: '',
+    name: '',
+    operation: '',
+    parentId: '',
+    projectId: '',
+    serial: 0,
+    templateNumber: 0
+  })
+  const [ ruleModal, setRuleModal ] = useState<iModal>({
+    visible: false,
+    loading: false
+  })
+  const onCreate = async (values: any, type: any) => {
+    setModalObj({
+      ...modalObj,
+      confirmLoading: true
+    })
+    const { code = -1 } = await apiResfulContract(type, values)
+    if (code === consts.RET_CODE.SUCCESS) {
+      // contractStore.updateContract(section)
+      contractStore.resetTree(tenderStore.bid)
+      if (type === 'return') {
+        contractStore.changeUpdate('2')
+      }
+    }
+    setModalObj({
+      ...modalObj,
+      type: '',
+      visible: false,
+      confirmLoading: false
+    })
+  }
+  const modalHandler = (type: string) => {
+    setModalObj({
+      ...modalObj,
+      visible: true,
+      type
+    })
+  }
+  const treeResfulApiHandler = async (type: string, payload: any) => {
+    if (type === 'add') {
+      return contractStore.addRowTree(payload.id)
+    }
+    const { code = -1, section = {} } = await apiResfulContractTree(type, payload)
+    if ( code === consts.RET_CODE.SUCCESS) {
+      contractStore.resetTree(tenderStore.bid)
+      if (type !== 'add' && type !== 'del') {
+        setRow({ ...row, ...section })
+      }
+    }
+  }
+
+  const onRuleCreate = async (ruleValue: any) => {
+    setRuleModal({ ...ruleModal, loading: true })
+    const { code = -1 } = await apiSaveRule({ bidsectionId: tenderStore.bid, type: 'contract_rule', rule: ruleValue })
+    if (code === consts.RET_CODE.SUCCESS) {
+      message.success("规则更改成功!")
+    }
+    setRuleModal({ ...ruleModal, loading: false, visible: false })
+  }
+
+  // 切换到删除合同弹窗
+  const reloadHandler = (type: string) => {
+    setModalObj({ ...modalObj, visible: false })
+    setTimeout(() => {
+      setModalObj({ ...modalObj, visible: true, type })
+    }, 300)
+  }
+  const showCBtn = useMemo(() => {
+    return !row.children && !row.contractCode && row.id
+  }, [ row ])
 
   return (
-    <div>
+    <div className="wrap-contaniner">
+      <Header title="维护项目节:">
+        <Slot position="left">
+          <div className="pi-flex-row">
+            {
+              row.id ?
+              <>
+                <Tooltip title="添加子项">
+                  <Button type="text" icon={<SvgIcon type="xxh-plus" style={{ color: '#007bff', fontSize: 12 }}></SvgIcon>} onClick={() => treeResfulApiHandler('add', { id: row.id, bidsectionId: row.bidsectionId })}></Button>
+                </Tooltip>
+                {
+                  !row.children?.length ?
+                  <Tooltip title="删除">
+                    <Button type="text" icon={<SvgIcon type="xxh-times" style={{ color: '#007bff', fontSize: 12 }}></SvgIcon>} onClick={() => treeResfulApiHandler('del', { id: row.id, bidsectionId: row.bidsectionId })} />
+                  </Tooltip>
+                  : <span className="pi-width-32 pi-height-32 pi-pd-tb-4 pi-flex-row-center"><SvgIcon type="xxh-times" style={{ color: '#9a9a9a', fontSize: 12 }}></SvgIcon></span>
+                }
+                {
+                  row.parentId && row.parentId !== contractTreeBaseId ?
+                  <Tooltip title="升级">
+                    <Button type="text" icon={<SvgIcon type="xxh-arrow-left" style={{ color: '#007bff', fontSize: 12 }} onClick={() => treeResfulApiHandler('depth', { id: row.id, bidsectionId: row.bidsectionId, operation: 'upDepth' })}></SvgIcon>}></Button>
+                  </Tooltip>
+                  :
+                  <span className="pi-width-32 pi-height-32 pi-pd-tb-4 pi-flex-row-center"><SvgIcon type="xxh-arrow-left" style={{ color: '#9a9a9a', fontSize: 12 }}/></span>
+                }
+                {
+                  row.elderBrother ?
+                  <Tooltip title="降级">
+                    <Button type="text" icon={<SvgIcon type="xxh-arrow-right" style={{ color: '#007bff', fontSize: 12 }}/>} onClick={() => treeResfulApiHandler('depth', { id: row.id, bidsectionId: row.bidsectionId, operation: 'downDepth' })} ></Button>
+                  </Tooltip>
+                  :
+                  <span className="pi-width-32 pi-height-32 pi-pd-tb-4 pi-flex-row-center"><SvgIcon type="xxh-arrow-right" style={{ color: '#9a9a9a', fontSize: 12 }}/></span>
+                }
+                {
+                  row.elderBrother ?
+                  <Tooltip title="上移">
+                    <Button type="text" icon={<SvgIcon type="xxh-arrow-up" style={{ color: '#007bff', fontSize: 12 }}/>} onClick={() => treeResfulApiHandler('serial', { id: row.id, bidsectionId: row.bidsectionId, operation: 'upSerial' })}  disabled={!row.elderBrother}></Button>
+                  </Tooltip>
+                  :
+                  <span className="pi-width-32 pi-height-32 pi-pd-tb-4 pi-flex-row-center"><SvgIcon type="xxh-arrow-up" style={{ color: '#9a9a9a', fontSize: 12 }}/></span>
+                }
+                {
+                  !row.isEnd ?
+                  <Tooltip title="下移">
+                    <Button type="text" icon={<SvgIcon type="xxh-arrow-down" style={{ color: '#007bff', fontSize: 12 }}/>} onClick={() => treeResfulApiHandler('serial', { id: row.id, bidsectionId: row.bidsectionId, operation: 'downSerial' })} disabled={row.isEnd}></Button>
+                  </Tooltip>
+                  :
+                  <span className="pi-width-32 pi-height-32 pi-pd-tb-4 pi-flex-row-center"><SvgIcon type="xxh-arrow-down" style={{ color: '#9a9a9a', fontSize: 12 }}/></span>
+                }
+              </>
+              : ''
+            }
+          </div>
+        </Slot>
+        <Slot position="right">
+          <Button type="ghost" size="small" icon={<SettingOutlined />} className="pi-mg-right-3" style={{ color: '#007bff' }} onClick={() => setRuleModal({ ...ruleModal, visible: true })}>设置</Button>
+          {
+            showCBtn ? <Button type="primary" size="small" onClick={() => setModalObj({ ...modalObj, type: 'create', visible: true })}>新建收入合同</Button>
+            : ""
+          }
+        </Slot>
+      </Header>
+      <TableContent modalHandler={modalHandler} row={row} setRow={(record: iIncomeTree) => setRow({ ...row, ...record })}></TableContent>
+      <RuleModal
+        type={consts.RULE.CONTRACT}
+        title="合同管理编号设置"
+        visible={ruleModal.visible}
+        onCreate={onRuleCreate}
+        loading={ruleModal.loading}
+        onCancel={() => setRuleModal({ ...ruleModal, visible: false })}
+      ></RuleModal>
+      <ContractModal row={row} modalObj={modalObj} onConfirm={onCreate} onCancel={ () => setModalObj({ ...modalObj, visible: false })} reload={(type) => reloadHandler(type)}></ContractModal>
     </div>
   )
 }
-
-export default observer(Spending)