Просмотр исходного кода

feat: 标段管理->新增文件夹、新增标段功能

lanjianrong 4 лет назад
Родитель
Сommit
eedbf941eb

+ 1 - 1
.vscode/.settings.json

@@ -2,7 +2,7 @@
   "path-intellisense.mappings": {
     "@": "${workspaceRoot}"
   },
-
+  // "typescript.suggest.paths": false,
   "editor.formatOnSave": false,
   "css.validate": false,
   "scss.validate": false,

+ 1 - 0
package.json

@@ -178,6 +178,7 @@
     "axios": "^0.20.0",
     "nprogress": "^0.2.0",
     "react": "^16.13.1",
+    "react-activation": "^0.5.5",
     "react-dom": "^16.13.1",
     "react-redux": "^7.2.1",
     "react-router-dom": "^5.2.0",

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
src/assets/icons/svg/edit.svg


Разница между файлами не показана из-за своего большого размера
+ 11 - 0
src/assets/icons/svg/exchange-alt.svg


+ 1 - 0
src/assets/icons/svg/plus.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"/></svg>

+ 1 - 0
src/assets/icons/svg/times.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"/></svg>

+ 11 - 6
src/components/SvgIcon/index.scss

@@ -1,7 +1,12 @@
-.svg-class {
-  display: inline-block;
-  width: 1em;
-  height: 1em;
-  overflow: hidden;
-  vertical-align: baseline;
+.svg-icon {
+  line-height: 1em;
+  vertical-align: text-bottom;
+  .svg-class {
+    display: inline-block;
+    width: 1em;
+    height: 1em;
+    margin-right: 5px;
+    overflow: hidden;
+    vertical-align: baseline;
+  }
 }

+ 4 - 3
src/components/SvgIcon/index.tsx

@@ -4,6 +4,7 @@ type svgProps = {
   iconClass: string,
   fontSize: string,
   fill: string
+  transform?: string
 } & typeof defaultProps
 
 const defaultProps = {
@@ -18,9 +19,9 @@ const SvgIcon = (props: svgProps) => {
     minWidth: fontSize + 'px'
   }
   return (
-    <span aria-hidden="true">
-      <svg className='svg-class' style={styleObj} fill={fill}>
-        <use xlinkHref={"#icon-" + iconClass}  />
+    <span aria-hidden="true" className="svg-icon">
+      <svg className='svg-class' style={styleObj} fill={fill} >
+        <use xlinkHref={"#icon-" + iconClass}  transform={ props.transform ? props.transform : ''}/>
       </svg>
     </span>
   )

+ 26 - 0
src/pages/Management/Tender/api.ts

@@ -7,3 +7,29 @@ export async function apiTree() {
   const { data } = await request.get('/api/tree')
   return data
 }
+
+/**
+ * 新建文件夹
+ */
+interface iNewFolder {
+  id: string
+  depth: number
+  name: string
+}
+export async function apiNewFolder(payload: iNewFolder) {
+  const { data } = await request.post('/api/tree/create', payload)
+  return data
+}
+
+/**
+ * 新建标段
+ */
+interface iNewTender {
+  folderId: string
+  depth: number
+  name: string
+}
+export async function apiNewTender(payload: iNewTender) {
+  const { data } = await request.post('/api/bidsection/create', payload)
+  return data
+}

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

@@ -0,0 +1,91 @@
+import { Form, Input, message, Modal } from 'antd'
+import React, { useEffect } from 'react'
+
+interface Option {
+  value: string | number;
+  label?: React.ReactNode;
+  disabled?: boolean;
+  children?: Option[];
+}
+interface iModalObj {
+  visible: boolean
+  type: 'folder' | 'tender' | 'root'
+  confirmLoading: boolean
+  pid: string
+}
+interface iModalFormProps {
+  modalObj:iModalObj
+  onCreate: (values, type ) => void
+  onCancel: () => void
+}
+
+const ModalForm: React.FC<iModalFormProps> = ({
+  onCreate,
+  onCancel,
+  modalObj: { visible, confirmLoading, type, pid }
+}) => {
+  const [ form ] = Form.useForm()
+  const modalObj = {
+    title: type === 'tender' ? '添加新标段' : '新建文件夹',
+    nameTitle:  type === 'tender' ? '标段名称' : '文件夹名称',
+    namePlaceholder:  type === 'tender' ? '输入标段名称' : '输入文件夹名称'
+  }
+  useEffect(() => {
+    form.resetFields()
+  }, [ modalObj ])
+  return (
+    <Modal
+      getContainer={false}
+      visible={visible}
+      confirmLoading={confirmLoading}
+      title={modalObj.title}
+      okText="确认添加"
+      cancelText="取消"
+      onCancel={onCancel}
+      closable={false}
+      onOk={() => {
+        form.validateFields().then((values) => {
+          form.resetFields()
+          onCreate(values, type)
+        }).catch(info => {
+          message.error(`Validate Failed:${info}`)
+        })
+    }}>
+      <Form form={form} layout="vertical">
+        <Form.Item name={type === 'tender' ? 'folderId' : 'id'} initialValue={pid} hidden>
+          <Input></Input>
+            {/* <TreeSelect treeData={cascaderArrOption} placeholder={modalObj.seletePlaceholder} disabled={type === 'root'} allowClear ></TreeSelect> */}
+        </Form.Item>
+        <Form.Item  name="name" label={modalObj.nameTitle} rules={[ { required: true, message: '请输入文件夹名称!' },{
+          validator: async (_, names) => {
+            if (names && names.length > 30) {
+              return Promise.reject(new Error('名称超过30个字,请缩减名称。'))
+            }
+          }
+        } ]}>
+          <Input allowClear size="small" autoComplete="off"></Input>
+        </Form.Item>
+        <Form.Item name="depth" initialValue={type === 'root' ? -1 : 0} hidden>
+          <Input></Input>
+        </Form.Item>
+      </Form>
+    </Modal>
+  )
+}
+
+// function mapTree(treeObj:TenderTree, type: 'folder' | 'tender' | 'root') {
+//   if (type === 'root') {
+//     return [ { value: treeObj.id, title: treeObj.name } ]
+//   }
+//   return treeObj.children?.map((tree:TenderTree) => {
+//     const newTree = { value: tree.id, title: tree.name }
+//     if ((type === 'folder' && !tree.isBid) || (type === 'tender' && tree.leaf)) {
+//       if (tree.children?.length) {
+//         newTree.children = mapTree(tree.children, type)
+//       }
+//       return newTree
+//     }
+//   }).filter(item => item) // filter的作用是过滤掉map最终可能产生的underfined的item(map的数组有多少个item就会返回多少个item,没有返回就是underfined)
+// }
+
+export default ModalForm

+ 10 - 0
src/pages/Management/Tender/index.scss

@@ -10,3 +10,13 @@
     line-height: 1.2;
   }
 }
+.menu-item {
+  font-size: 12px;
+  color: #007bff;
+  &.text-muted {
+    color: #6c757d !important;
+    &:hover {
+      cursor: 'none';
+    }
+  }
+}

+ 121 - 53
src/pages/Management/Tender/index.tsx

@@ -1,37 +1,33 @@
 import Header from '@/components/Header'
 import Slot from '@/components/Header/slot'
 import SvgIcon from '@/components/SvgIcon'
-import { TenderTree } from '@/types/tender'
+import { iModalProps, iTenderFormValue, TenderTree } from '@/types/tender'
 import consts from '@/utils/consts'
-import { FolderAddFilled } from '@ant-design/icons'
-import { Button, Table } from 'antd'
+import { CaretDownOutlined, FolderAddFilled } from '@ant-design/icons'
+import { Button, Dropdown, Menu, Table, Tooltip } from 'antd'
 import { ColumnsType } from 'antd/lib/table'
 import React, { useEffect, useState } from 'react'
 import { Link } from 'react-router-dom'
-import { apiTree } from './api'
+import { apiNewFolder, apiNewTender, apiTree } from './api'
+import ModalForm from './components/ModalForm'
 import styles from './index.module.scss'
 import './index.scss'
 const Tender: React.FC<{}> = () =>{
-
-  return (
-    <div className="content-wrap">
-      <Header title="标段管理">
-        <Slot position="right">
-          <Button type="primary" size="small" icon={<FolderAddFilled />}>新建文件夹</Button>
-        </Slot>
-      </Header>
-      <Content/>
-    </div>
-  )
-}
-
-
-const Content: React.FC<{}> = () => {
-  const [ tree, setTree ] = useState<TenderTree[]>([])
-  // const column_name_render = (text: string, record: any, index: number) => {
-  //   console.log(1)
-  //   return (<span>{text}</span>)
-  // }
+  const [ tree, setTree ] = useState<TenderTree>({})
+  const [ modal, setModal ] = useState<iModalProps>({
+    type: '',
+    visible: false,
+    confirmLoading: false,
+    pid: ''
+  })
+  const addBtnClick = (type: 'folder' | 'tender' | 'root', pid: string) => {
+    setModal({
+      ...modal,
+      type,
+      visible: true,
+      pid
+    })
+  }
   const columns: ColumnsType<TenderTree>  = [
     {
       title: '名称',
@@ -52,7 +48,7 @@ const Content: React.FC<{}> = () => {
       key: 'member',
       width: 200,
       // eslint-disable-next-line react/display-name
-      render: (text:string, record: TenderTree, index: number) => {
+      render: (text:string, record: TenderTree) => {
         if (record.leaf) {
           return <div><span className="pi-mg-right-5">{record.ancounts} 成员</span><Link className={styles.treeBtn} to="/">成员管理</Link></div>
         }
@@ -62,46 +58,118 @@ const Content: React.FC<{}> = () => {
       title: '操作',
       dataIndex: 'opreate',
       key: 'opreate',
-      width: 80
+      width: 80,
       // eslint-disable-next-line react/display-name
-      // render: (text:string, record: TenderTree, index: number) => {
-      //   return <Dropdown overlay={(record: TenderTree, index: number) => {
-      //     return (
-      //       <Menu>
-      //         <Menu.Item key="0"></Menu.Item>
-      //         <Menu.Item key="1"></Menu.Item>
-      //         <Menu.Item key="2"></Menu.Item>
-      //         <Menu.Item key="3"></Menu.Item>
-      //       </Menu>
-      //     )
-      //   }} trigger={['click']}>
-      //     <span>
-      //       <SvgIcon iconClass="align-center" fill="#007bff" fontSize="12"></SvgIcon>
-      //       <CaretDownOutlined style={{ fontSize: "12px", color: "#007bff" }}/>
-      //     </span>
-      //   </Dropdown>
-      // }
+      render: (text:string, record: TenderTree) => {
+        return <Dropdown overlay={() => {
+          return (
+            <Menu>
+              <Menu.Item key="0"><div className="menu-item"><SvgIcon iconClass="edit" fontSize="12"></SvgIcon><span>重命名</span></div></Menu.Item>
+              <Menu.Item key="1"><div className="menu-item"><SvgIcon iconClass="exchange-alt" fontSize="12"></SvgIcon><span>移动</span></div></Menu.Item>
+              {
+                record.children?.length ?
+                  <Menu.Item key="2">
+                    <Tooltip placement="left" title="请先移除所有数据">
+                      <div className="menu-item text-muted">
+                        <SvgIcon iconClass="times" fontSize="12"></SvgIcon><span>删除</span>
+                      </div>
+                    </Tooltip>
+                  </Menu.Item>
+                :
+                <Menu.Item key="2"><div className="menu-item"><SvgIcon iconClass="times" fontSize="12"></SvgIcon><span>删除</span></div></Menu.Item>
+              }
+              {
+                record.isfolder ? <Menu.Divider /> : ''
+              }
+              {
+                record.isfolder ? record.leaf ?
+                <Menu.Item key="3"><div className= "menu-item" onClick={() => addBtnClick('tender', record.id)}><SvgIcon iconClass="plus" fontSize="12"></SvgIcon><span>新建标段</span></div></Menu.Item>
+                :
+                <Menu.Item key="3">
+                  <Tooltip placement="left" title="标段无法与文件夹同层">
+                    <div className= "menu-item text-muted"><SvgIcon iconClass="plus" fontSize="12"></SvgIcon><span>新建标段</span></div>
+                  </Tooltip></Menu.Item>
+                : ''
+              }
+              {
+                record.isfolder ? !record.isBid ?
+                <Menu.Item key="4"><div className="menu-item" onClick={() => addBtnClick('folder', record.id)}><SvgIcon iconClass="folder" fontSize="12"></SvgIcon><span>新建子文件夹</span></div></Menu.Item>
+                :
+                <Menu.Item key="4">
+                  <Tooltip placement="left" title="文件夹无法与标段同层">
+                    <div className="menu-item text-muted"><SvgIcon iconClass="folder" fontSize="12"></SvgIcon><span>新建子文件夹</span></div>
+                  </Tooltip>
+                </Menu.Item>
+                : ''
+              }
+
+            </Menu>
+          )
+        }} trigger={[ 'click' ]}>
+          <span>
+            <SvgIcon iconClass="align-center" fill="#007bff" fontSize="12"></SvgIcon>
+            <CaretDownOutlined style={{ fontSize: "12px", color: "#007bff" }}/>
+          </span>
+        </Dropdown>
+      }
     }
   ]
+  const onCreate = async (values: iTenderFormValue, type: 'folder' | 'tender' | 'root') => {
+    console.log('Received values of form: ', values, type)
+    setModal({
+      ...modal,
+      confirmLoading: true
+    })
+    if (type === 'tender') {
+      const { code = -1 } = await apiNewTender(values)
+      if (code === consts.RET_CODE.SUCCESS) {
+        getTree()
+      }
+    } else {
+      const { code = -1 } = await apiNewFolder(values)
+      if (code === consts.RET_CODE.SUCCESS) {
+        getTree()
+      }
+    }
+    setModal({
+      ...modal,
+      confirmLoading: false,
+      visible: false,
+      pid: ''
+    })
+
+  }
   useEffect(() => {
     getTree()
   }, [])
   const getTree = async () => {
-    const { data: { children = [] } = [], code = -1 } = await apiTree()
+    const { data = {}, code = -1 } = await apiTree()
     if (code === consts.RET_CODE.SUCCESS) {
-      setTree(children)
+      setTree(data)
     }
   }
-
   return (
-    <div className="pi-flex-column">
-      <div className="pi-mg-16 pi-pd-tb-64 pi-pd-lr-32 jumbotron">
-        <h3>没有标段数据</h3>
-      </div>
-      <div className={styles.tableContent}>
-        <Table<TenderTree> columns={columns} dataSource={tree} pagination={false} rowKey={record => record.id} bordered></Table>
+    <div className="content-wrap">
+      <Header title="标段管理">
+        <Slot position="right">
+          <Button type="primary" size="small" icon={<FolderAddFilled />} onClick={() => addBtnClick('root', tree.id)}>新建文件夹</Button>
+        </Slot>
+      </Header>
+      <div className="pi-flex-column">
+        <div className="pi-mg-16 pi-pd-tb-64 pi-pd-lr-32 jumbotron">
+          <h3>没有标段数据</h3>
+        </div>
+        <div className={styles.tableContent}>
+          <Table<TenderTree> columns={columns} dataSource={tree.children} pagination={false} rowKey={record => record.id} bordered></Table>
+        </div>
+        <ModalForm modalObj={modal}  treeObj={tree} onCreate={onCreate} onCancel={() => setModal({
+          ...modal,
+          visible: false
+        })} ></ModalForm>
       </div>
     </div>
   )
 }
-export default  Tender
+
+
+export default Tender

+ 11 - 1
src/router/Guard.tsx

@@ -43,7 +43,6 @@ const NavigationGuards:React.FC<NavigationGuardsProps> = props => {
 function switchRoute (pathConfig: string, pathTarget: string):boolean {
   if (pathConfig === pathTarget) return true
   const reg = new RegExp(`(^${pathConfig})(?=/)`)
-
   return reg.test(pathTarget)
 }
 
@@ -91,7 +90,18 @@ function findTargetRoute (parentPath: any, targetPath: string, routeConfig: Rout
   for (let i = 0; i < routeConfig.length; i++) {
     const item = routeConfig[i]
     const path = combinationPath(parentPath, item.path)
+
     if (targetPath && switchRoute(path, targetPath)) {
+      // if (targetPath !== path) {
+      //   const childRoutes: RouteModol[] = item.childRoutes.map(child => {
+      //     return { ...child, path: combinationPath(item.path, child.path) }
+      //   })
+      //   const routerIdx = childRoutes.findIndex(item => item.path === targetPath)
+
+      //   if (routerIdx !== -1) {
+      //     return childRoutes[routerIdx]
+      //   }
+      // }
       return { ...item, path }
     }
   }

+ 15 - 1
src/types/tender.d.ts

@@ -9,6 +9,7 @@ export type TenderTree = {
   attribution: string;
   isfolder: number;
   createTime: string;
+  isBid: boolean;
   updateTime: string;
   moveId: string;
   ancounts: number;
@@ -16,5 +17,18 @@ export type TenderTree = {
   leaf: boolean;
   isEnd: boolean;
   childsTotal: number;
-  children: any[] | null;
+  children: TenderTree[] | null;
+}
+
+export interface iTenderFormValue {
+  [propName: 'folderId' | 'id'] : string
+  name: string
+  depth: number
+}
+
+export interface iModalProps {
+  type: string
+  visible: boolean
+  confirmLoading: boolean
+  pid: string
 }

+ 3 - 7
tsconfig.json

@@ -8,7 +8,7 @@
     "allowSyntheticDefaultImports": true,
     "strict": true,
     "forceConsistentCasingInFileNames": true,
-    "module": "esnext",
+    "module": "commonjs",
     "moduleResolution": "node",
     "resolveJsonModule": true,
     "isolatedModules": false,
@@ -17,13 +17,9 @@
     "noImplicitThis": false,
     "jsx": "react",
     "baseUrl": "./src",
+    "typeRoots": ["./types/*"],
     "paths": {
-      "@/*": ["./*"],
-      "components/*": ["./src/components/*"],
-      "store/*": ["./src/store/*"],
-      "pages/*": ["./src/pages/*"],
-      "router/*": ["./src/router/*"],
-      "utils/*": ["./src/utils/*"]
+      "@/*": ["./*"]
     }
   },
   "include": ["src"]

Разница между файлами не показана из-за своего большого размера
+ 0 - 10806
yarn.lock