ソースを参照

feat: rule & workbench page

hi-cactus! 3 年 前
コミット
38a6be9763
25 ファイル変更3489 行追加371 行削除
  1. 2 2
      app/containers/App/sagas.ts
  2. 238 0
      app/containers/DataGovernanceAuaitAnalysis/AnalysisReportDetailModal.tsx
  3. 113 0
      app/containers/DataGovernanceAuaitAnalysis/index.less
  4. 401 2
      app/containers/DataGovernanceAuaitAnalysis/index.tsx
  5. 58 0
      app/containers/DataGovernanceAuaitAnalysis/types.ts
  6. 207 0
      app/containers/DataGovernanceMarkRule/components/AuditClassificationFormModal.tsx
  7. 151 0
      app/containers/DataGovernanceMarkRule/components/DataSubjectFormModal.tsx
  8. 220 0
      app/containers/DataGovernanceMarkRule/components/RuleFormModal.tsx
  9. 252 4
      app/containers/DataGovernanceMarkRule/index.tsx
  10. 47 0
      app/containers/DataGovernanceMarkRule/types.ts
  11. 87 0
      app/containers/DataGovernanceQualityAudit/components/ClassificationsFormModal.tsx
  12. 199 0
      app/containers/DataGovernanceQualityAudit/components/MetadataModal.tsx
  13. 147 0
      app/containers/DataGovernanceQualityAudit/components/QualityTaskFormModal.tsx
  14. 149 0
      app/containers/DataGovernanceQualityAudit/components/ScheduleFormModal.tsx
  15. 333 2
      app/containers/DataGovernanceQualityAudit/index.tsx
  16. 42 0
      app/containers/DataGovernanceQualityAudit/types.ts
  17. 128 0
      app/containers/DataManagerDictionary/components/DictDataFormModal.tsx
  18. 157 47
      app/containers/DataManagerDictionary/components/DictDatasModal.tsx
  19. 35 17
      app/containers/DataManagerDictionary/index.tsx
  20. 1 3
      app/containers/DataManagerDictionary/types.ts
  21. 18 17
      app/containers/DataManagerView/components/CatalogueModal.tsx
  22. 8 8
      app/containers/DataManagerView/index.less
  23. 260 168
      app/containers/DataManagerView/index.tsx
  24. 187 100
      app/containers/View/Editor.tsx
  25. 49 1
      app/utils/api.ts

+ 2 - 2
app/containers/App/sagas.ts

@@ -112,7 +112,7 @@ export function* getServerConfigurations(action) {
     yield put(serverConfigurationsGetted(configurations))
   } catch (err) {
     console.log(err)
-    window.location.href = 'http://taihu.xt.wenhq.top:8083'
+    // window.location.href = 'http://taihu.xt.wenhq.top:8083'
     yield put(getServerConfigurationsFail(err))
     errorHandler(err)
   }
@@ -420,7 +420,7 @@ export default function* rootGroupSaga() {
       GET_CAPTCHA_FOR_RESET_PASSWORD,
       getCaptchaForResetPassword as any
     ),
-    takeEvery(RESET_PASSWORD_UNLOGGED, resetPasswordUnlogged  as any),
+    takeEvery(RESET_PASSWORD_UNLOGGED, resetPasswordUnlogged as any),
     takeEvery(GET_USER_BY_TOKEN, getUserByToken),
     takeEvery(JOIN_ORGANIZATION, joinOrganization),
     takeLatest(LOAD_DOWNLOAD_LIST, getDownloadList),

+ 238 - 0
app/containers/DataGovernanceAuaitAnalysis/AnalysisReportDetailModal.tsx

@@ -0,0 +1,238 @@
+import { Button, DatePicker, Drawer, Modal, Spin, Table } from 'antd'
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import { IReport } from 'containers/DataGovernanceAuaitAnalysis/types'
+import { init, EChartOption, ECharts } from 'echarts'
+import request from 'utils/request'
+import api from 'utils/api'
+import moment, { Moment } from 'moment'
+import { RangePickerValue } from 'antd/lib/date-picker/interface'
+import { ColumnProps } from 'antd/lib/table'
+
+// tslint:disable-next-line:interface-name
+export interface AnalysisReportDetailModalProps {
+  visible: boolean
+  // eslint-disable-next-line no-undef
+  formView: Partial<IReport>
+  tableColumns: Array<ColumnProps<IReport>>
+}
+
+const DEFAULT_COLORS = ['#52c41a', '#fa8c16', '#1890ff', '#722ed1']
+
+export const AnalysisReportDetailModal = ({
+  visible,
+  formView,
+  tableColumns
+}: AnalysisReportDetailModalProps) => {
+  const lineChartRef = useRef<ECharts>()
+  const lineChartElementRef = useRef<HTMLDivElement>()
+
+  const [lineLoading, setLineLoading] = useState(false)
+  const [lineReports, setLineReports] = useState<IReport[]>([])
+  const [lineDateRange, setLineDateRange] = useState<RangePickerValue>([])
+
+  const [tableReports, setTableReports] = useState<IReport[]>([])
+  const [tableLoading, setTableLoading] = useState(false)
+  const [tableDateRange, setTableDateRange] = useState<RangePickerValue>([])
+
+  const lineDataSource = useMemo(
+    () =>
+      lineReports.map((l) => ({
+        ...l,
+        完整性正确率: Number(l.integrityCorrectProbability ?? '') * 100,
+        一致性正确率: Number(l.uniformityCorrectProbability ?? '') * 100,
+        规范性正确率: Number(l.normativeCorrectProbability ?? '') * 100,
+        准确性正确率: Number(l.accuracyCorrectProbability ?? '') * 100
+      })),
+    [lineReports]
+  )
+
+  const lineOption = useMemo<EChartOption>(
+    () => ({
+      tooltip: {},
+      legend: {
+        data: ['完整性正确率', '一致性正确率', '规范性正确率', '准确性正确率']
+      },
+      xAxis: {
+        type: 'category'
+      },
+      yAxis: {
+        type: 'value'
+      },
+      series: [
+        '完整性正确率',
+        '一致性正确率',
+        '规范性正确率',
+        '准确性正确率'
+      ].map((o, idx) => ({
+        name: o,
+        type: 'line',
+        smooth: true,
+        data: lineDataSource.map((s) => s[o]),
+        itemStyle: {
+          color: DEFAULT_COLORS[idx]
+        }
+      }))
+    }),
+    [lineDataSource]
+  )
+  const disableDate = (current: Moment) =>
+    current &&
+    (current < moment(formView?.startTime) ||
+      current > moment(formView?.endTime))
+
+  const queryLineReports = async (startTime: string, endTime: string) => {
+    try {
+      setLineLoading(true)
+
+      const data = await request(
+        `${
+          api.qualityReportDetail + formView?.cronJobId
+        }?startTime=${startTime}&endTime=${endTime}`,
+        { method: 'GET' }
+      )
+      // @ts-ignore
+      setLineReports(data.payload)
+    } finally {
+      setLineLoading(false)
+    }
+  }
+  const queryTableReports = async (startTime: string, endTime: string) => {
+    try {
+      setTableLoading(true)
+
+      const data = await request(
+        `${
+          api.qualityReportDetail + formView?.cronJobId
+        }?startTime=${startTime}&endTime=${endTime}`,
+        { method: 'GET' }
+      )
+      // @ts-ignore
+      setTableReports(data.payload)
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  useEffect(() => {
+    if (lineDateRange?.[0] && lineDateRange?.[1]) {
+      // @ts-ignore
+      queryLineReports(
+        // @ts-ignore
+        lineDateRange?.[0]?.format('YYYY-MM-DD'),
+        // @ts-ignore
+        lineDateRange?.[1]?.format('YYYY-MM-DD')
+      )
+    }
+  }, [lineDateRange])
+
+  useEffect(() => {
+    if (tableDateRange?.[0] && tableDateRange?.[1]) {
+      // @ts-ignore
+      queryTableReports(
+        // @ts-ignore
+        tableDateRange?.[0]?.format('YYYY-MM-DD'),
+        // @ts-ignore
+        tableDateRange?.[1]?.format('YYYY-MM-DD')
+      )
+    }
+  }, [tableDateRange])
+
+  useEffect(() => {
+    setLineDateRange([
+      // @ts-ignore
+      formView?.startTime ? moment(formView.startTime) : undefined,
+      // @ts-ignore
+      formView?.endTime ? moment(formView.endTime) : undefined
+    ])
+    setTableDateRange([
+      // @ts-ignore
+      formView?.startTime ? moment(formView.startTime) : undefined,
+      // @ts-ignore
+      formView?.endTime ? moment(formView.endTime) : undefined
+    ])
+  }, [formView?.startTime, formView?.endTime])
+
+  useEffect(() => {
+    if (formView?.startTime && formView?.endTime) {
+      queryLineReports(formView.startTime, formView.endTime)
+      queryTableReports(formView.startTime, formView.endTime)
+    }
+  }, [formView?.startTime, formView?.endTime])
+
+  useEffect(() => {
+    if (lineChartElementRef.current) {
+      lineChartRef.current = init(lineChartElementRef.current)
+      lineChartRef.current.setOption(lineOption)
+    }
+  }, [lineOption])
+
+  return (
+    <Drawer
+      placement="bottom"
+      visible={visible}
+      height="100%"
+      destroyOnClose
+      title="质量任务结果"
+    >
+      <div>
+        <div
+          style={{
+            display: 'flex',
+            justifyContent: 'space-between',
+            alignItems: 'center',
+            margin: '10px 0'
+          }}
+        >
+          <h4 style={{ borderLeft: '2px solid #1890ff', paddingLeft: 14 }}>
+            数据质量趋势
+          </h4>
+          <div>
+            <DatePicker.RangePicker
+              value={lineDateRange}
+              disabledDate={disableDate}
+              onChange={(range) => setLineDateRange(range)}
+              format="YYYY-MM-DD"
+            />
+            <span style={{ padding: 4 }} />
+            <Button>查询</Button>
+          </div>
+        </div>
+        <Spin spinning={lineLoading}>
+          <div style={{ height: 400 }} ref={lineChartElementRef} />
+        </Spin>
+        <div
+          style={{
+            display: 'flex',
+            justifyContent: 'space-between',
+            alignItems: 'center',
+            margin: '10px 0'
+          }}
+        >
+          <h4 style={{ borderLeft: '2px solid #1890ff', paddingLeft: 14 }}>
+            数据质量历史
+          </h4>
+          <div>
+            <DatePicker.RangePicker
+              value={tableDateRange}
+              disabledDate={disableDate}
+              onChange={(range) => setTableDateRange(range)}
+              format="YYYY-MM-DD"
+            />
+            <span style={{ padding: 4 }} />
+            <Button>查询</Button>
+          </div>
+        </div>
+        <Table
+          style={{ flex: 1 }}
+          bordered
+          rowKey="id"
+          loading={tableLoading}
+          dataSource={tableReports}
+          columns={tableColumns}
+          pagination={false}
+          // onChange={this.tableChange}
+        />
+      </div>
+    </Drawer>
+  )
+}

+ 113 - 0
app/containers/DataGovernanceAuaitAnalysis/index.less

@@ -0,0 +1,113 @@
+.audit-analysiss-search-content {
+  display: flex;
+  align-items: center;
+
+  & > * {
+    margin-right: 10px;
+  }
+}
+
+.audit-analysiss-card-content {
+  display: flex;
+  margin: 40px 0 40px;
+
+  .audit-analysiss-card-item {
+    flex: 1;
+    margin: 0 10px;
+    background-color: #fafafa;
+    border-radius: 6px;
+    padding: 10px;
+    font-size: 12px;
+
+    &:first-child {
+      margin-left: 0;
+    }
+
+    &:last-child {
+      margin-right: 0;
+    }
+
+    .audit-analysiss-card-item-title {
+      padding-left: 14px;
+      position: relative;
+      font-size: 16px;
+
+      &:before {
+        content: '';
+        position: absolute;
+        width: 4px;
+        height: 100%;
+        top: 0;
+        left: 0;
+        border-radius: 10px;
+        background-color: #1890ff;
+      }
+    }
+
+    .audit-analysiss-card-item-rate {
+      .audit-analysiss-card-item-rate-label {
+        margin: 4px 0;
+        display: flex;
+        justify-content: space-between;
+        align-items: end;
+
+        & > span:last-child {
+          color: #1890ff;
+          font-size: 16px;
+        }
+      }
+    }
+
+    .audit-analysiss-card-item-number {
+      display: flex;
+      //justify-content: space-between;
+      margin-top: 4px;
+
+      .audit-analysiss-card-item-number-total,
+      .audit-analysiss-card-item-number-count {
+        display: flex;
+        flex-direction: column;
+        flex: 1;
+
+        & > span:last-child {
+          color: #1890ff;
+          font-size: 20px;
+        }
+      }
+    }
+
+    &:nth-child(2) {
+      .audit-analysiss-card-item-title:before {
+        background-color: #52c41a;
+      }
+
+      .audit-analysiss-card-item-rate
+        .audit-analysiss-card-item-rate-label
+        > span:last-child {
+        color: #52c41a !important;
+      }
+    }
+
+    &:nth-child(3) {
+      .audit-analysiss-card-item-title:before {
+        background-color: #fa8c16;
+      }
+      .audit-analysiss-card-item-rate
+        .audit-analysiss-card-item-rate-label
+        > span:last-child {
+        color: #fa8c16 !important;
+      }
+    }
+
+    &:nth-child(5) {
+      .audit-analysiss-card-item-title:before {
+        background-color: #722ed1;
+      }
+      .audit-analysiss-card-item-rate
+        .audit-analysiss-card-item-rate-label
+        > span:last-child {
+        color: #722ed1 !important;
+      }
+    }
+  }
+}

+ 401 - 2
app/containers/DataGovernanceAuaitAnalysis/index.tsx

@@ -1,13 +1,412 @@
-import React from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
 import Helmet from 'react-helmet'
 import Container, { ContainerBody } from 'components/Container'
+import Box from 'components/Box'
+import { Button, DatePicker, message, Progress, Select, Table } from 'antd'
+import { ColumnProps } from 'antd/lib/table'
+import { IReport } from 'containers/DataGovernanceAuaitAnalysis/types'
+import api from 'utils/api'
+import request from 'utils/request'
+import styles from './index.less'
+import { AnalysisReportDetailModal } from 'containers/DataGovernanceAuaitAnalysis/AnalysisReportDetailModal'
+import { IClassification } from 'containers/DataGovernanceQualityAudit/types'
+import moment, { Moment } from 'moment'
 
 export default function DataGovernanceAuaitAnalysis() {
+  const [tableLoading, setTableLoading] = useState(false)
+  const [detailVisible, setDetailVisible] = useState(false)
+  const [detailForm, setDetailForm] = useState<IReport>()
+
+  const [reports, setReports] = useState<IReport[]>([])
+
+  const [classifications, setClassifications] = useState<IClassification[]>([])
+  const [systemId, setSystemId] = useState<number>(null)
+  const [time, setTime] = useState<Moment>(moment())
+
+  const workbench = useMemo(() => {
+    const total = reports
+      .map(
+        (item) =>
+          item.accuracyCorrect ??
+          0 + item.accuracyError ??
+          0 + item.integrityCorrect ??
+          0 + item.integrityError ??
+          0 + item.normativeCorrect ??
+          0 + item.normativeError ??
+          0 + item.uniformityCorrect ??
+          0 + item.uniformityError ??
+          0
+      )
+      .reduce((c, n) => c + n, 0)
+
+    const totalCorrect = reports
+      .map(
+        (item) =>
+          item.accuracyCorrect ??
+          0 + item.integrityCorrect ??
+          0 + item.normativeCorrect ??
+          0 + item.uniformityCorrect ??
+          0
+      )
+      .reduce((c, n) => c + n, 0)
+
+    // 完整性
+    const integrityCorrect = reports
+      .map((item) => item.integrityCorrect ?? 0)
+      .reduce((c, n) => c + n, 0)
+    const integrityTotal = reports
+      .map((item) => item.integrityCorrect ?? 0 + item.integrityError)
+      .reduce((c, n) => c + n, 0)
+
+    // 一致性
+    const uniformityCorrect = reports
+      .map((item) => item.uniformityCorrect ?? 0)
+      .reduce((c, n) => c + n, 0)
+    const uniformityTotal = reports
+      .map((item) => item.uniformityCorrect ?? 0 + item.uniformityError)
+      .reduce((c, n) => c + n, 0)
+
+    // 规范性
+    const normativeCorrect = reports
+      .map((item) => item.normativeCorrect ?? 0)
+      .reduce((c, n) => c + n, 0)
+    const normativeTotal = reports
+      .map((item) => item.normativeCorrect ?? 0 + item.normativeError)
+      .reduce((c, n) => c + n, 0)
+    // 准确性
+    const accuracyCorrect = reports
+      .map((item) => item.accuracyCorrect ?? 0)
+      .reduce((c, n) => c + n, 0)
+    const accuracyTotal = reports
+      .map((item) => item.accuracyCorrect ?? 0 + item.accuracyError)
+      .reduce((c, n) => c + n, 0)
+    return [
+      {
+        title: '总概率',
+        rateLabel: '正确率',
+        rateValue: ((totalCorrect / total || 0) * 100).toFixed(2) ?? '',
+        countLabel: '正确数量',
+        countValue: totalCorrect ?? 0,
+        totalLabel: '总数据量',
+        totalValue: total ?? 0,
+        color: '#1890ff'
+      },
+      {
+        title: '完整性',
+        rateLabel: '正确率',
+        rateValue:
+          ((integrityCorrect / integrityTotal || 0) * 100).toFixed(2) ?? '',
+        countLabel: '正确数量',
+        countValue: integrityCorrect,
+        totalLabel: '总数据量',
+        totalValue: integrityTotal,
+        color: '#52c41a'
+      },
+      {
+        title: '一致性',
+        rateLabel: '正确率',
+        rateValue:
+          ((uniformityCorrect / uniformityTotal || 0) * 100).toFixed(2) ?? '',
+        countLabel: '正确数量',
+        countValue: uniformityCorrect,
+        totalLabel: '总数据量',
+        totalValue: uniformityTotal,
+        color: '#fa8c16'
+      },
+      {
+        title: '规范性',
+        rateLabel: '正确率',
+        rateValue:
+          ((normativeCorrect / normativeTotal || 0) * 100).toFixed(2) ?? '',
+        countLabel: '正确数量',
+        countValue: normativeCorrect,
+        totalLabel: '总数据量',
+        totalValue: normativeTotal,
+        color: '#1890ff'
+      },
+      {
+        title: '准确性',
+        rateLabel: '正确率',
+        rateValue:
+          ((accuracyCorrect / accuracyTotal || 0) * 100).toFixed(2) ?? '',
+        countLabel: '正确数量',
+        countValue: accuracyCorrect,
+        totalLabel: '总数据量',
+        totalValue: accuracyTotal,
+        color: '#722ed1'
+      }
+    ]
+  }, [reports])
+  const tableColumns: Array<ColumnProps<IReport>> = [
+    {
+      title: '任务名称',
+      dataIndex: 'taskName'
+    },
+    {
+      title: '稽查时间',
+      dataIndex: 'auditorTime'
+    },
+    {
+      title: '系统名称',
+      dataIndex: 'typeName'
+    },
+    {
+      title: '元数据名称',
+      dataIndex: 'metadataName'
+    },
+    {
+      title: '完整性',
+      children: [
+        {
+          title: '正确数量',
+          dataIndex: 'integrityCorrect'
+        },
+        {
+          title: '错误数量',
+          dataIndex: 'integrityError'
+        },
+        {
+          title: '正确率',
+          dataIndex: 'integrityCorrectProbability'
+        },
+        {
+          title: '错误率',
+          dataIndex: 'integrityErrorProbability'
+        }
+      ]
+    },
+    {
+      title: '一致性',
+      children: [
+        {
+          title: '正确数量',
+          dataIndex: 'uniformityCorrect'
+        },
+        {
+          title: '错误数量',
+          dataIndex: 'uniformityError'
+        },
+        {
+          title: '正确率',
+          dataIndex: 'uniformityCorrectProbability'
+        },
+        {
+          title: '错误率',
+          dataIndex: 'uniformityErrorProbability'
+        }
+      ]
+    },
+    {
+      title: '规范性',
+      children: [
+        {
+          title: '正确数量',
+          dataIndex: 'normativeCorrect'
+        },
+        {
+          title: '错误数量',
+          dataIndex: 'normativeError'
+        },
+        {
+          title: '正确率',
+          dataIndex: 'normativeCorrectProbability'
+        },
+        {
+          title: '错误率',
+          dataIndex: 'normativeErrorProbability'
+        }
+      ]
+    },
+    {
+      title: '准确性',
+      children: [
+        {
+          title: '正确数量',
+          dataIndex: 'accuracyCorrect'
+        },
+        {
+          title: '错误数量',
+          dataIndex: 'accuracyError'
+        },
+        {
+          title: '正确率',
+          dataIndex: 'accuracyCorrectProbability'
+        },
+        {
+          title: '错误率',
+          dataIndex: 'accuracyErrorProbability'
+        }
+      ]
+    },
+    {
+      title: '操作',
+      fixed: 'right',
+      width: 50,
+      render: (_, data) => (
+        <span
+          onClick={() => {
+            setDetailVisible(true)
+            setDetailForm(data)
+          }}
+        >
+          详情
+        </span>
+      )
+    }
+  ]
+
+  const queryReports = async () => {
+    if (!systemId || !time) {
+      return
+    }
+    try {
+      setTableLoading(true)
+      const data = await request(
+        `${api.qualityReport}?systemId=${systemId}&time=${time.format(
+          'YYYY-MM-DD'
+        )}`,
+        {
+          method: 'GET'
+        }
+      )
+      // @ts-ignore
+      setReports(data?.payload ?? [])
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const querySystem = async () => {
+    try {
+      setTableLoading(true)
+      const data = await request(`${api.getAuditClassification}`, {
+        method: 'GET'
+      })
+      // @ts-ignore
+      setClassifications(data?.payload ?? [])
+      // @ts-ignore
+      setSystemId(data?.payload?.[0].id)
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const handleQuery = () => {
+    if (!systemId || !time) {
+      message.info({ content: '请输入主题或时间' })
+      return
+    }
+    queryReports()
+  }
+
+  useEffect(() => {
+    async function query() {
+      await querySystem()
+      await queryReports()
+    }
+
+    query()
+  }, [])
+
   return (
     <Container>
       <Helmet title="稽核分析" />
+      <ContainerBody>
+        <Box>
+          <Box.Header>
+            <Box.Title>稽核分析</Box.Title>
+          </Box.Header>
+          <div style={{ padding: 20 }} />
+          <Box.Body>
+            <div className={styles['audit-analysiss-search-content']}>
+              <div>
+                <span>系统选择:</span>
+                <Select
+                  value={systemId}
+                  style={{ width: 200 }}
+                  onChange={(e) => setSystemId(e)}
+                >
+                  {classifications.map((c) => (
+                    <Select.Option key={c.id} value={c.id}>
+                      {c.name}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </div>
+              <div>
+                <span>时间选择:</span>
+                <DatePicker
+                  onChange={(e) => setTime(e)}
+                  value={time}
+                  format="YYYY-MM-DD"
+                />
+              </div>
+              <Button onClick={handleQuery}>查询</Button>
+            </div>
+
+            <div className={styles['audit-analysiss-card-content']}>
+              {workbench?.map((item, idx) => (
+                <div
+                  key={item.title}
+                  className={styles['audit-analysiss-card-item']}
+                >
+                  <div className={styles['audit-analysiss-card-item-title']}>
+                    {item.title}
+                  </div>
+                  <div className={styles['audit-analysiss-card-item-rate']}>
+                    <div
+                      className={styles['audit-analysiss-card-item-rate-label']}
+                    >
+                      <span>正确率</span>
+                      <span>{item.rateValue ?? '-'}%</span>
+                    </div>
+                    <Progress
+                      showInfo={false}
+                      strokeColor={item.color}
+                      percent={Number(item.rateValue ?? 0)}
+                    />
+                  </div>
+                  <div className={styles['audit-analysiss-card-item-number']}>
+                    <div
+                      className={
+                        styles['audit-analysiss-card-item-number-count']
+                      }
+                    >
+                      <span>正确数量</span>
+                      <span>{item.countValue ?? '-'}</span>
+                    </div>
+
+                    <div
+                      className={
+                        styles['audit-analysiss-card-item-number-total']
+                      }
+                    >
+                      <span>总数据量</span>
+                      <span>{item.totalValue ?? '-'}</span>
+                    </div>
+                  </div>
+                </div>
+              ))}
+            </div>
 
-      <ContainerBody>稽核分析</ContainerBody>
+            <Table
+              style={{ flex: 1 }}
+              bordered
+              rowKey="id"
+              loading={tableLoading}
+              dataSource={reports}
+              columns={tableColumns}
+              pagination={false}
+            />
+            <div style={{ padding: 20 }} />
+            <AnalysisReportDetailModal
+              visible={detailVisible}
+              tableColumns={tableColumns}
+              formView={detailForm}
+            />
+          </Box.Body>
+        </Box>
+      </ContainerBody>
     </Container>
   )
 }

+ 58 - 0
app/containers/DataGovernanceAuaitAnalysis/types.ts

@@ -0,0 +1,58 @@
+export interface IReport {
+  // 必须 准确性正确数
+  accuracyCorrect: number
+  //  必须准确性正确率
+  accuracyCorrectProbability: string
+  //   必须准确性错误数
+  accuracyError: number
+  // 必须 准确性 错误率
+  accuracyErrorProbability: string
+  // 必须
+  auditorCode: string
+  // 必须 稽核时间
+  auditorTime: string
+  // 必须 任务id
+  cronJobId: number
+  //  必须
+  endTime: string
+  //  必须
+  id: number
+  // 必须 完整性正确数
+  integrityCorrect: number
+  // 必须 完整性正确率
+  integrityCorrectProbability: string
+  // 必须 完整性错误数
+  integrityError: number
+  // 必须 完整性错误率
+  integrityErrorProbability: string
+  // 必须
+  metadataConfig: string
+  // 必须
+  metadataName: string
+  // 必须 规范性正确数
+  normativeCorrect: number
+  // 必须 规范性正确率
+  normativeCorrectProbability: string
+  // 必须 规范性错误数
+  normativeError: number
+  // 必须 规范性错误率
+  normativeErrorProbability: string
+  // 必须
+  pId: number
+  // 必须
+  startTime: string
+  // 必须 任务名称
+  taskName: string
+  // 必须 系统名称
+  typeName: string
+  // 必须 一致性正确数
+  uniformityCorrect: number
+  // 必须 一致性正确率
+  uniformityCorrectProbability: string
+  // 必须 一致性错误数
+  uniformityError: number
+  // 必须 一致性错误率
+  uniformityErrorProbability: string
+  // 必须
+  viewId: number
+}

+ 207 - 0
app/containers/DataGovernanceMarkRule/components/AuditClassificationFormModal.tsx

@@ -0,0 +1,207 @@
+import React, { useEffect, useState } from 'react'
+import { Modal, message, Button, Tabs, Table, Divider, Popconfirm } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IDataRule, IDataSubject, IRule } from '../types'
+import { ColumnProps } from 'antd/lib/table'
+import api from 'utils/api'
+import request from 'utils/request'
+import RuleFormModal from 'containers/DataGovernanceMarkRule/components/RuleFormModal'
+import Messages from 'containers/Display/messages'
+
+interface IAuditClassificationFormModalProps {
+  visible: boolean
+  fromView: IDataSubject
+  onCancel: () => void
+}
+
+const { TabPane } = Tabs
+
+// @ts-ignore
+const enum InspectionType {
+  integrity = 'integrity', // 完整性
+  uniformity = 'uniformity', // 一致性
+  normative = 'normative', // 规范性
+  accuracy = 'accuracy' // 准确性
+}
+
+export const AuditClassificationFormModal = (
+  props: IAuditClassificationFormModalProps
+) => {
+  const { visible, fromView, onCancel } = props
+
+  const [activeKey, setActiveKey] =
+    useState<IDataRule['inspectionType']>('integrity')
+  const [tableLoading, setTableLoading] = useState(false)
+  const [formVisible, setFormVisible] = useState(false)
+  const [formLoading, setFormLoading] = useState(false)
+  const [auditClassification, setAuditClassification] = useState<IRule[]>([])
+  const [ruleFormView, setRuleFormView] = useState<IDataRule>({})
+
+  const queryDataSubject = async () => {
+    try {
+      setTableLoading(true)
+      const data = await request(
+        api.getDataRules + `?subjectId=${fromView.id}`,
+        { method: 'GET' }
+      )
+      // @ts-ignore
+      setAuditClassification(data?.payload ?? [])
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  useEffect(() => {
+    // tslint:disable-next-line:no-unused-expression
+    visible && queryDataSubject()
+  }, [visible])
+
+  const tableColumns: Array<ColumnProps<IRule>> = [
+    {
+      title: '规则编号',
+      dataIndex: 'ruleCode'
+    },
+    {
+      title: '规则名称',
+      dataIndex: 'ruleName'
+    },
+    {
+      title: '检测类型',
+      dataIndex: 'inspectionType'
+    },
+    {
+      title: '规则描述',
+      dataIndex: 'ruleDesc'
+    },
+    {
+      title: '操作',
+      render: (_, data) => (
+        <>
+          <a onClick={() => handleAddEditRule(data)}>编辑</a>
+          <Divider type={'vertical'} />
+          <Popconfirm
+            title={'确定删除?'}
+            placement={'bottom'}
+            onConfirm={() => handleDeleteTheme(data)}
+          >
+            <a>删除</a>
+          </Popconfirm>
+        </>
+      )
+    }
+  ]
+
+  const handleTabChange = (e) => setActiveKey(e)
+  const handleParse = (str: string): IDataRule['ruleConfig'] => {
+    try {
+      return JSON.parse(str)
+    } catch (e) {
+      console.log(e)
+    }
+    return {}
+  }
+  const handleSave = async (form: IDataRule) => {
+    try {
+      setFormLoading(true)
+      const url = ruleFormView?.id
+        ? api.updateDataRules + ruleFormView.id
+        : api.createDataRules
+      console.log(form, url, '===')
+      const data = await request(url, {
+        method: ruleFormView?.id ? 'PUT' : 'POST',
+        data: {
+          subjectId: fromView.id,
+          inspectionType: activeKey,
+          id: ruleFormView.id,
+          ...form,
+          ruleConfig: JSON.stringify(form.ruleConfig)
+        }
+      })
+      console.log(data)
+      // @ts-ignore
+      if (data?.header?.code === 200) {
+        setFormVisible(false)
+        queryDataSubject()
+      }
+    } finally {
+      setFormLoading(false)
+    }
+  }
+  const handleAddEditRule = (data) => {
+    setRuleFormView({ ...data, ruleConfig: handleParse(data.ruleConfig) })
+    setFormVisible(true)
+  }
+  const handleDeleteTheme = async (data: IRule) => {
+    try {
+      setTableLoading(true)
+      const result = await request(api.deleteDataSubject + data.id)
+      // @ts-ignore
+      if (result?.header?.code === 200) {
+        message.success({ content: '删除成功' })
+        queryDataSubject()
+      }
+    } finally {
+      setTableLoading(false)
+    }
+  }
+  const handleCancel = () => setFormVisible(false)
+
+  return (
+    <Modal
+      title={'质量稽核规则'}
+      visible={visible}
+      footer={null}
+      onCancel={onCancel}
+      width={1000}
+      destroyOnClose
+    >
+      <div style={{ padding: '20px 0 0' }}>
+        <Button
+          type="primary"
+          icon="plus"
+          onClick={() => {
+            setFormVisible(true)
+            // DataSubjectFormVisible(true)
+            // DataSubjectForm(null)
+          }}
+        >
+          新增
+        </Button>
+      </div>
+      <Tabs activeKey={activeKey} onChange={handleTabChange}>
+        <TabPane tab={'完整性'} key={'integrity'}>
+          <Table
+            bordered
+            rowKey={'id'}
+            loading={tableLoading}
+            dataSource={auditClassification.filter(
+              (o) => o.inspectionType === InspectionType.integrity
+            )}
+            columns={tableColumns}
+            pagination={false}
+          />
+        </TabPane>
+        <TabPane tab="一致性" key={'uniformity'}>
+          <Table
+            bordered
+            rowKey={'id'}
+            loading={tableLoading}
+            dataSource={auditClassification.filter(
+              (o) => o.inspectionType === InspectionType.uniformity
+            )}
+            columns={tableColumns}
+            pagination={false}
+          />
+        </TabPane>
+      </Tabs>
+      <RuleFormModal
+        visible={formVisible}
+        inspectionType={activeKey}
+        onCancel={handleCancel}
+        loading={formLoading}
+        fromView={ruleFormView}
+        onSave={handleSave}
+      />
+    </Modal>
+  )
+}

+ 151 - 0
app/containers/DataGovernanceMarkRule/components/DataSubjectFormModal.tsx

@@ -0,0 +1,151 @@
+import React from 'react'
+import { Modal, Form, Button, Input, Switch, Select } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IDataSubject } from '../types'
+import { IDictData } from 'containers/DataManagerDictionary/types'
+
+const FormItem = Form.Item
+
+interface IDataSubjectFormModalModalProps
+  extends FormComponentProps<IDataSubject> {
+  subject: string | number
+  subjects: IDictData[]
+  visible: boolean
+  loading: boolean
+  fromView: IDataSubject
+  onSave: (view: IDataSubject) => void
+  onCancel: () => void
+}
+
+export class DataSubjectFormModal extends React.PureComponent<IDataSubjectFormModalModalProps> {
+  // @ts-ignore
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, fromView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const copyView: IDataSubject = { ...fieldsValue }
+      onSave(copyView)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render() {
+    const { form, visible, loading, fromView, onCancel, subjects, subject } =
+      this.props
+    const { getFieldDecorator } = form
+
+    const modalButtons = [
+      <Button key={'back'} size={'large'} onClick={onCancel}>
+        取 消
+      </Button>,
+      <Button
+        disabled={loading}
+        key={'submit'}
+        size={'large'}
+        type={'primary'}
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    ]
+
+    console.log(
+      fromView,
+      subjects?.filter((e) => e.dictCode !== -1),
+      '---'
+    )
+
+    console.log(fromView?.pId, subject, '====')
+
+    return (
+      <Modal
+        title={fromView ? '编辑' : '新增'}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+        destroyOnClose
+      >
+        <Form>
+          <FormItem label={'标准主题'} {...this.formItemStyle} required>
+            {getFieldDecorator<IDataSubject>('pId', {
+              initialValue: subject === -1 ? fromView?.pId : subject,
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(
+              <Select disabled={subject && subject !== -1}>
+                {subjects
+                  ?.filter((e) => e.dictCode !== -1)
+                  ?.map((d) => (
+                    <Select.Option key={d.dictCode} value={d.dictCode}>
+                      {d.dictLabel}
+                    </Select.Option>
+                  ))}
+              </Select>
+            )}
+          </FormItem>
+          <FormItem label={'标准编码'} {...this.formItemStyle} required>
+            {getFieldDecorator<IDataSubject>('standardCode', {
+              initialValue: fromView?.standardCode,
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(<Input />)}
+          </FormItem>
+          <FormItem label={'标准名称'} {...this.formItemStyle} required>
+            {getFieldDecorator<IDataSubject>('standardName', {
+              initialValue: fromView?.standardName,
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(<Input />)}
+          </FormItem>
+          <FormItem label={'别名'} {...this.formItemStyle}>
+            {getFieldDecorator<IDataSubject>('standardAlias', {
+              initialValue: fromView?.standardAlias
+            })(<Input />)}
+          </FormItem>
+          <FormItem label={'管理人员'} {...this.formItemStyle}>
+            {getFieldDecorator<IDataSubject>('management', {
+              initialValue: fromView?.management
+            })(<Input />)}
+          </FormItem>
+          <FormItem label={'管理部门'} {...this.formItemStyle}>
+            {getFieldDecorator<IDataSubject>('deptName', {
+              initialValue: fromView?.deptName
+            })(<Input />)}
+          </FormItem>
+          <FormItem label={'英文名称'} {...this.formItemStyle}>
+            {getFieldDecorator<IDataSubject>('englishName', {
+              initialValue: fromView?.englishName
+            })(<Input />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<IDataSubjectFormModalModalProps>()(
+  DataSubjectFormModal
+)

+ 220 - 0
app/containers/DataGovernanceMarkRule/components/RuleFormModal.tsx

@@ -0,0 +1,220 @@
+import React from 'react'
+import {
+  Modal,
+  Form,
+  Button,
+  Input,
+  Switch,
+  Radio,
+  InputNumber,
+  Select
+} from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IDataRule, IDataSubject } from '../types'
+
+const FormItem = Form.Item
+
+interface IRuleFormModalProps extends FormComponentProps<IDataRule> {
+  visible: boolean
+  loading: boolean
+  fromView: IDataRule
+  onSave: (view: IDataRule) => void
+  onCancel: () => void
+  inspectionType: IDataRule['inspectionType']
+}
+
+class NumberRange extends React.Component<{
+  value?: { from?: number; to?: number }
+  onChange?: (value: { from?: number; to?: number }) => void
+}> {
+  public handleNumberChange = (num, type: 'from' | 'to') => {
+    this.triggerChange({ [type]: num })
+  }
+
+  public triggerChange = (changedValue: { from?: number; to?: number }) => {
+    const { onChange, value } = this.props
+    if (onChange) {
+      onChange({
+        ...value,
+        ...changedValue
+      })
+    }
+  }
+
+  public render() {
+    const { value } = this.props
+    return (
+      <span>
+        <InputNumber
+          value={value?.from}
+          onChange={(val) => this.handleNumberChange(val, 'from')}
+        />
+        &nbsp;&nbsp;~&nbsp;&nbsp;
+        <InputNumber
+          value={value?.to}
+          onChange={(val) => this.handleNumberChange(val, 'to')}
+        />
+      </span>
+    )
+  }
+}
+
+class RuleFormModal extends React.PureComponent<IRuleFormModalProps> {
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, fromView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const data: IDataRule = { ...fieldsValue }
+
+      // @ts-ignore
+      onSave({
+        ...data,
+        ruleConfig: {
+          [data.ruleType]:
+            data.ruleType === 'range'
+              ? {
+                  // @ts-ignore
+                  from: data.ruleConfig.from,
+                  // @ts-ignore
+                  to: data.ruleConfig.to
+                }
+              : data.ruleConfig
+        }
+      })
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  private checkRange = (rule, value, callback) => {
+    if (value?.from < value?.to) {
+      return callback()
+    }
+    callback('to must greater than from!')
+  }
+  private handleSetUUID = () =>
+    this.props.form.setFieldsValue({
+      ruleCode: Date.now() + ''
+    })
+
+  public render() {
+    const { form, visible, loading, fromView, onCancel, inspectionType } =
+      this.props
+    const { getFieldDecorator } = form
+
+    const modalButtons = [
+      <Button key="back" size="large" onClick={onCancel}>
+        取 消
+      </Button>,
+      <Button
+        disabled={loading}
+        key="submit"
+        size="large"
+        type="primary"
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    ]
+
+    console.log(fromView, '+++')
+
+    return (
+      <Modal
+        title={fromView ? '编辑' : '新增'}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+        destroyOnClose
+      >
+        <Form>
+          <FormItem label="规则编号" {...this.formItemStyle} required>
+            {getFieldDecorator<IDataRule>('ruleCode', {
+              initialValue: fromView?.ruleCode
+            })(<Input />)}
+            {!fromView?.id && (
+              <Button onClick={this.handleSetUUID}>自动生成编号</Button>
+            )}
+          </FormItem>
+          <FormItem label="规则名称" {...this.formItemStyle} required>
+            {getFieldDecorator<IDataRule>('ruleName', {
+              initialValue: fromView?.ruleName
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="规则描述" {...this.formItemStyle}>
+            {getFieldDecorator<IDataRule>('ruleDesc', {
+              initialValue: fromView?.ruleDesc
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="规则类型" {...this.formItemStyle}>
+            {getFieldDecorator<IDataRule>('ruleType', {
+              initialValue: fromView?.ruleType
+            })(
+              <Radio.Group>
+                {inspectionType === 'integrity' && (
+                  <Radio value="not_null">非空</Radio>
+                )}
+                {inspectionType === 'uniformity' && (
+                  <Radio value="range">值域</Radio>
+                )}
+                {inspectionType === 'uniformity' && (
+                  <Radio value="length">长度</Radio>
+                )}
+                {inspectionType === 'uniformity' && (
+                  <Radio value="date_format">日期格式</Radio>
+                )}
+                {inspectionType === 'uniformity' && (
+                  <Radio value="">自定义</Radio>
+                )}
+              </Radio.Group>
+            )}
+          </FormItem>
+          {this.props.form.getFieldValue('ruleType') === 'range' && (
+            <FormItem label="值域" {...this.formItemStyle}>
+              {getFieldDecorator<IDataRule>('ruleConfig', {
+                initialValue: fromView?.ruleConfig?.range,
+                rules: [{ validator: this.checkRange }]
+              })(<NumberRange />)}
+            </FormItem>
+          )}
+          {this.props.form.getFieldValue('ruleType') === 'length' && (
+            <FormItem label="长度" {...this.formItemStyle}>
+              {getFieldDecorator<IDataRule>('ruleConfig', {
+                initialValue: fromView?.ruleConfig?.length
+              })(<InputNumber />)}
+            </FormItem>
+          )}
+          {this.props.form.getFieldValue('ruleType') === 'date_format' && (
+            <FormItem label="日期格式" {...this.formItemStyle}>
+              {getFieldDecorator<IDataRule>('ruleConfig', {
+                initialValue: fromView?.ruleConfig?.date_format
+              })(
+                <Select>
+                  <Select.Option value="YYYY-MM-DD">YYYY-MM-DD</Select.Option>
+                  <Select.Option value="YYYY-MM-DD HH:mm">
+                    YYYY-MM-DD HH:mm
+                  </Select.Option>
+                  <Select.Option value="YYYY-MM-DD HH:mm:ss">
+                    YYYY-MM-DD HH:mm:ss
+                  </Select.Option>
+                </Select>
+              )}
+            </FormItem>
+          )}
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<IRuleFormModalProps>()(RuleFormModal)

+ 252 - 4
app/containers/DataGovernanceMarkRule/index.tsx

@@ -1,13 +1,261 @@
-import React from 'react'
-import Container, { ContainerBody } from 'components/Container'
+import React, { useEffect, useState } from 'react'
 import Helmet from 'react-helmet'
+import Container, { ContainerBody } from 'components/Container'
+import Box from 'components/Box'
+import { Button, Divider, Icon, message, Popconfirm, Spin, Table } from 'antd'
+import { ColumnProps } from 'antd/lib/table'
+import { compose } from 'redux'
+import request from 'utils/request'
+import api from 'utils/api'
+import { IDataSubject } from 'containers/DataGovernanceMarkRule/types'
+import DataSubjectFormModal from 'containers/DataGovernanceMarkRule/components/DataSubjectFormModal'
+import { AuditClassificationFormModal } from 'containers/DataGovernanceMarkRule/components/AuditClassificationFormModal'
+import styles from 'containers/DataManagerView/index.less'
+import classnames from 'classnames'
+import { IDictData } from 'containers/DataManagerDictionary/types'
+import { isNumber } from 'lodash'
+
+function DataDictionary() {
+  const [tableLoading, setTableLoading] = useState(false)
+  const [dataSubject, setDataSubject] = useState<IDataSubject[]>([])
+
+  const [dataSubjectFormVisible, setDataSubjectFormVisible] = useState(false)
+  const [dataSubjectFormLoading, setDataSubjectFormLoading] = useState(false)
+  const [dataSubjectForm, setDataSubjectForm] = useState<IDataSubject>(null)
+
+  const [auditLoading, setAuditLoadingLoading] = useState(false)
+
+  const [selectedSubjectsKey, setSelectedSubjectsKey] = useState<number>()
+
+  const [treeLoading, setTreeLoading] = useState(false)
+  const [subjects, setSubject] = useState<IDictData[]>([
+    { dictLabel: '全部主题', dictCode: -1 }
+  ])
+
+  const tableColumns: Array<ColumnProps<IDataSubject>> = [
+    {
+      title: '标准主题',
+      dataIndex: 'standardTheme'
+    },
+    {
+      title: '标准编码',
+      dataIndex: 'standardCode'
+    },
+    {
+      title: '标准名称',
+      dataIndex: 'standardName'
+    },
+    {
+      title: '别名',
+      dataIndex: 'standardAlias'
+    },
+    {
+      title: '英文名称',
+      dataIndex: 'englishName'
+    },
+    {
+      title: '管理部门',
+      dataIndex: 'deptName'
+    },
+    {
+      title: '管理人员',
+      dataIndex: 'management'
+    },
+    {
+      title: '操作',
+      render: (_, data) => (
+        <>
+          <a
+            onClick={() => {
+              setAuditLoadingLoading(true)
+              setDataSubjectForm(data)
+            }}
+          >
+            编辑
+          </a>
+          <Divider type="vertical" />
+          <a
+            onClick={() => {
+              setDataSubjectFormVisible(true)
+              setDataSubjectForm(data)
+            }}
+          >
+            修改
+          </a>
+          <Divider type="vertical" />
+          <Popconfirm
+            title="确定删除?"
+            placement="bottom"
+            onConfirm={() => handleDeleteDataSubject(data.id)}
+          >
+            <a>删除</a>
+          </Popconfirm>
+        </>
+      )
+    }
+  ]
+
+  const querySubjects = async () => {
+    try {
+      setTreeLoading(true)
+      const data = await request(`${api.getSubjects}data_standard`, {
+        method: 'GET'
+      })
+      const defaultSubject = [{ dictLabel: '全部主题', dictCode: -1 }]
+      setSubject(
+        // @ts-ignore
+        data?.payload ? [...defaultSubject, ...data?.payload] : defaultSubject
+      )
+      setSelectedSubjectsKey(-1)
+    } finally {
+      setTreeLoading(false)
+    }
+  }
+
+  const queryDataBySelectedSubject = async () => {
+    try {
+      setTableLoading(true)
+      const data = await request(
+        `${api.getDataSubject}?pId=${
+          selectedSubjectsKey === -1 ? '' : selectedSubjectsKey
+        }`,
+        { method: 'GET' }
+      )
+      // @ts-ignore
+      setDataSubject(data?.payload ?? [])
+    } finally {
+      setTableLoading(false)
+    }
+  }
+  const handleDeleteDataSubject = async (id) => {
+    try {
+      setTableLoading(true)
+      await request(`${api.deleteDataSubject}/${id}`, { method: 'delete' })
+      await queryDataBySelectedSubject()
+    } finally {
+      setTableLoading(false)
+    }
+  }
+  const handleSaveDataSubject = async (dt: IDataSubject) => {
+    // // 0=正常,1=停用
+    try {
+      setDataSubjectFormLoading(true)
+      const url = dataSubjectForm?.id
+        ? `${api.updateDataSubject}/${dataSubjectForm.id}`
+        : api.createDataSubject
+      await request(url, {
+        method: dataSubjectForm?.id ? 'PUT' : 'POST',
+        data: { ...(dataSubjectForm ?? {}), ...dt }
+      })
+      message.success({ content: '成功' })
+      setDataSubjectFormVisible(false)
+      await queryDataBySelectedSubject()
+    } finally {
+      setDataSubjectFormLoading(false)
+    }
+  }
+  const renderTree = (catalogues: IDictData[]) => (
+    <>
+      {catalogues.map((c, idx) => (
+        <div
+          key={c.dictCode ?? idx}
+          className={classnames(styles.treeNode, {
+            [styles.treeNodeSelected]: selectedSubjectsKey === c.dictCode
+          })}
+        >
+          <span
+            className={styles.treeNodeLeft}
+            onClick={() => {
+              setSelectedSubjectsKey(c.dictCode)
+            }}
+          >
+            <Icon type="folder-open" />
+            {c.dictLabel}
+          </span>
+        </div>
+      ))}
+    </>
+  )
+
+  useEffect(() => {
+    async function query() {
+      await querySubjects()
+      // await queryDataBySelectedSubject()
+    }
+
+    query()
+  }, [])
+
+  useEffect(() => {
+    if (isNumber(selectedSubjectsKey)) {
+      queryDataBySelectedSubject()
+    }
+  }, [selectedSubjectsKey])
 
-export default function DataGovernance() {
   return (
     <Container>
       <Helmet title="规则制定" />
+      <ContainerBody>
+        <Box>
+          <Box.Header>
+            <Box.Title>规则制定</Box.Title>
+          </Box.Header>
+          <Box.Body>
+            <div className={styles.treeTableContainer}>
+              <div className={styles.treeContainer}>
+                <div className={styles.treeTitle}>
+                  <h6>主题</h6>
+                </div>
+                <div className={styles.treeContent}>
+                  <Spin spinning={treeLoading}>{renderTree(subjects)}</Spin>
+                </div>
+              </div>
 
-      <ContainerBody>规则制定</ContainerBody>
+              <div style={{ flex: 1 }}>
+                <div style={{ padding: '0 0 20px' }}>
+                  <Button
+                    type="primary"
+                    icon="plus"
+                    onClick={() => {
+                      setDataSubjectFormVisible(true)
+                      setDataSubjectForm(null)
+                    }}
+                  >
+                    新增
+                  </Button>
+                </div>
+                <Table
+                  style={{ flex: 1 }}
+                  bordered
+                  rowKey="id"
+                  loading={tableLoading}
+                  dataSource={dataSubject}
+                  columns={tableColumns}
+                  pagination={false}
+                  // onChange={this.tableChange}
+                />
+              </div>
+            </div>
+            <br />
+          </Box.Body>
+        </Box>
+      </ContainerBody>
+      <AuditClassificationFormModal
+        visible={auditLoading}
+        fromView={dataSubjectForm}
+        onCancel={() => setAuditLoadingLoading(false)}
+      />
+      <DataSubjectFormModal
+        visible={dataSubjectFormVisible}
+        loading={dataSubjectFormLoading}
+        fromView={dataSubjectForm}
+        subject={selectedSubjectsKey}
+        subjects={subjects}
+        onSave={handleSaveDataSubject}
+        onCancel={() => setDataSubjectFormVisible(false)}
+      />
     </Container>
   )
 }
+
+export default compose()(DataDictionary)

+ 47 - 0
app/containers/DataGovernanceMarkRule/types.ts

@@ -0,0 +1,47 @@
+export interface IDataSubject {
+  deptName?: string //  非必须 管理部门
+  englishName?: string //   非必须  英文名称
+  id?: number //   非必须  主键id
+  management?: string //   非必须  管理人员
+  pId?: number //   非必须  父id-主题 id
+  standardAlias?: string //   非必须  别名
+  standardCode?: string //   非必须  标准编码
+  standardName?: string //   非必须  标准名称
+  standardTheme?: string //   非必须  标准主题
+}
+
+export interface IRule {
+  id: number
+  inspectionType: string // 类型 (完整性:integrity| 一致性:uniformity|normative:规范性|accuracy:准确性)
+  ruleCode: string // 规则编号
+  ruleConfig: string // JSON字符串” {range:{from:,to:},length:,date_format:}
+  ruleDesc: string // 规则描述
+  ruleName: string // 规则名称
+  ruleType: string // not_null: 非空 range: 值域 length:长度 date_format:日期格式
+  subjectId: number // 主题id
+}
+
+export interface IDataRule {
+  id?: string
+  // 非必须 规则编号
+  ruleCode?: string
+  // 非必须 规则名称
+  ruleName?: string
+  // 非必须 规则描述
+  ruleDesc?: string
+  // 非必须 主题id
+  subjectId?: number
+  // 必须 类型(完整性:integrity| 一致性:uniformity|normative:规范性|accuracy:准确性)
+  inspectionType?: 'integrity' | 'uniformity' | 'normative' | 'accuracy'
+  // 必须 not_null: 非空 range: 值域 length:长度 date_format:日期格式
+  ruleType?: string
+  // 必须 JSON字符串” {range:{from:,to:},length:,date_format:}
+  ruleConfig?: {
+    range?: {
+      from?: string | number
+      to?: string | number
+    }
+    length?: string | number
+    date_format?: string
+  }
+}

+ 87 - 0
app/containers/DataGovernanceQualityAudit/components/ClassificationsFormModal.tsx

@@ -0,0 +1,87 @@
+import React from 'react'
+import { Modal, Form, Button, Input, Switch, Select } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IClassification } from '../types'
+
+const FormItem = Form.Item
+
+interface IClassificationsFormModalProps
+  extends FormComponentProps<IClassification> {
+  visible: boolean
+  loading: boolean
+  formView: IClassification
+  onSave: (view: IClassification) => void
+  onCancel: () => void
+}
+
+class ClassificationsFormModal extends React.PureComponent<IClassificationsFormModalProps> {
+  // @ts-ignore
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, formView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const copyView: IClassification = { ...fieldsValue }
+      onSave(copyView)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render() {
+    const { form, visible, loading, formView, onCancel } = this.props
+    const { getFieldDecorator } = form
+
+    const modalButtons = [
+      <Button key={'back'} size={'large'} onClick={onCancel}>
+        取 消
+      </Button>,
+      <Button
+        disabled={loading}
+        key={'submit'}
+        size={'large'}
+        type={'primary'}
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    ]
+
+    return (
+      <Modal
+        title={formView?.id ? '编辑' : '新增'}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+        destroyOnClose
+      >
+        <Form>
+          <FormItem label={'稽核类型'} {...this.formItemStyle} required>
+            {getFieldDecorator<IClassification>('name', {
+              initialValue: formView?.name,
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(<Input />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<IClassificationsFormModalProps>()(
+  ClassificationsFormModal
+)

+ 199 - 0
app/containers/DataGovernanceQualityAudit/components/MetadataModal.tsx

@@ -0,0 +1,199 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React, { useEffect, useState } from 'react'
+import { Modal, Button, Table, Divider, Icon, Spin } from 'antd'
+import { ColumnProps } from 'antd/lib/table'
+import api from 'utils/api'
+import request from 'utils/request'
+import styles from 'containers/DataManagerView/index.less'
+import classnames from 'classnames'
+import { ICatalogue, IViewBase } from 'containers/DataManagerView/types'
+import { RouteComponentWithParams } from 'utils/types'
+import { withRouter } from 'react-router-dom'
+
+interface IMetadataModalProps extends RouteComponentWithParams {
+  visible: boolean
+  onCancel: () => void
+}
+
+const MetadataModal = (props: IMetadataModalProps) => {
+  const { visible, onCancel, match } = props
+
+  const [treeLoading, setTreeLoading] = useState(false)
+  const [selectedKey, setSelectedKey] = useState<number[]>([])
+  const [catalogues, setCatalogues] = useState<ICatalogue[]>([])
+
+  const [tableMetadata, setTableMetadata] = useState<IViewBase[]>([])
+  const [tableLoading, setTableLoading] = useState(false)
+  const [tableRowSelection, setTableRowSelection] = useState([])
+
+  const getCatalogues = async () => {
+    try {
+      const { projectId } = match.params
+
+      setTreeLoading(true)
+      // @ts-ignore
+      const { payload } = await request(
+        `${api.getCatalogues}?projectId=${projectId}`,
+        { method: 'GET' }
+      )
+      setCatalogues(payload)
+      setSelectedKey(payload?.[0]?.id ? [payload?.[0]?.id] : [])
+    } finally {
+      setTreeLoading(false)
+    }
+  }
+
+  const loadViews = async () => {
+    const { projectId } = match.params
+    if (projectId && selectedKey.length > 0) {
+      const parentId = Number(selectedKey[0])
+      try {
+        setTableLoading(true)
+        const data = await request(
+          `${api.getViewsByParentId}?projectId=${projectId}&parentId=${parentId}`,
+          { method: 'get' }
+        )
+        // @ts-ignore
+        setTableMetadata(data.payload ?? [])
+      } finally {
+        setTableLoading(false)
+      }
+    }
+  }
+
+  const tableColumns: Array<ColumnProps<{}>> = [
+    {
+      title: '元数据名称',
+      dataIndex: 'dictCode'
+    },
+    {
+      title: '元数据类型',
+      dataIndex: 'dictLabel'
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'dictValue'
+    },
+    {
+      title: '创建人',
+      dataIndex: 'dictSort'
+    },
+    {
+      title: '是否关联质量',
+      dataIndex: 'isDefault'
+    },
+    {
+      title: '',
+      render: () => (
+        <>
+          <a>编辑</a>
+          <Divider type="vertical" />
+          <a>删除</a>
+        </>
+      )
+    }
+  ]
+
+  const renderTree = (catalogues: ICatalogue[]) => (
+    <>
+      {catalogues.map((c, idx) => (
+        <>
+          <div
+            key={c.id ?? idx}
+            className={classnames(styles.treeNode, {
+              [styles.treeNodeSelected]: selectedKey.includes(c.id)
+            })}
+          >
+            <span
+              className={styles.treeNodeLeft}
+              onClick={() => {
+                setSelectedKey([c.id])
+              }}
+            >
+              <Icon type="file" />
+              {c.name}
+            </span>
+          </div>
+          <div style={{ marginLeft: 20 }}>
+            {c.children && renderTree(c.children)}
+          </div>
+        </>
+      ))}
+    </>
+  )
+
+  useEffect(() => {
+    getCatalogues()
+  }, [])
+  useEffect(() => {
+    if (selectedKey.length > 0) {
+      loadViews()
+    }
+  }, [selectedKey])
+
+  return (
+    <Modal
+      title="选择元数据"
+      visible={visible}
+      onCancel={onCancel}
+      maskClosable={false}
+      destroyOnClose
+      width="80%"
+    >
+      <div className={styles.treeTableContainer}>
+        <div className={styles.treeContainer}>
+          <div className={styles.treeTitle}>
+            <h6>选择元数据</h6>
+          </div>
+          <div className={styles.treeContent}>
+            <Spin spinning={treeLoading}>{renderTree(catalogues)}</Spin>
+          </div>
+        </div>
+
+        <div style={{ flex: 1 }}>
+          <div style={{ padding: '0 0 20px' }}>
+            <Button type="primary" icon="plus">
+              查询
+            </Button>
+          </div>
+          <Table
+            rowSelection={{
+              selectedRowKeys: tableRowSelection,
+              onChange: (selectedRowKeys) => {
+                setTableRowSelection(selectedRowKeys)
+              }
+            }}
+            style={{ flex: 1 }}
+            bordered
+            rowKey="id"
+            loading={tableLoading}
+            dataSource={tableMetadata}
+            columns={tableColumns}
+            pagination={false}
+          />
+        </div>
+      </div>
+    </Modal>
+  )
+}
+
+export default withRouter(MetadataModal)

+ 147 - 0
app/containers/DataGovernanceQualityAudit/components/QualityTaskFormModal.tsx

@@ -0,0 +1,147 @@
+import React, { useEffect, useState } from 'react'
+import {
+  Drawer,
+  Form,
+  Button,
+  Input,
+  Switch,
+  Select,
+  Tag,
+  Table,
+  Divider,
+  Dropdown,
+  Menu,
+  Popconfirm,
+  Icon
+} from 'antd'
+import { IClassification, IQualityTask } from '../types'
+import { ColumnProps } from 'antd/lib/table'
+import MetadataModal from 'containers/DataGovernanceQualityAudit/components/MetadataModal'
+
+interface IQualityTaskFormModalProps {
+  visible: boolean
+  loading: boolean
+  // eslint-disable-next-line no-undef
+  formView: Partial<IQualityTask>
+  onSave: (view: IClassification) => void
+  onCancel: () => void
+}
+
+const QualityTaskFormModal = ({
+  formView,
+  visible,
+  loading,
+  onCancel,
+  onSave
+}: IQualityTaskFormModalProps) => {
+  const [metaVisible, setMetaVisible] = useState(false)
+
+  const [taskName, setTaskName] = useState<IQualityTask['taskName']>()
+
+  const handleSave = () => {
+    const view: IQualityTaskFormModalProps['formView'] = {}
+    onSave(view)
+  }
+
+  const paramsColumns: Array<ColumnProps<IQualityTask>> = [
+    {
+      title: '名称',
+      dataIndex: 'taskName'
+    },
+    {
+      title: '数据类型',
+      dataIndex: 'taskName1'
+    },
+    {
+      title: '别名',
+      dataIndex: 'taskName2'
+    },
+    {
+      title: '标准名称',
+      dataIndex: 'taskName3'
+    },
+    {
+      title: '操作',
+      render: (_, data) => (
+        <>
+          <a>关联标准</a>
+          <Divider type="vertical" />
+          <a>取消关联</a>
+        </>
+      )
+    }
+  ]
+
+  useEffect(() => {
+    setTaskName(formView?.taskName)
+  }, [formView])
+
+  return (
+    <Drawer
+      title={null}
+      visible={visible}
+      onClose={onCancel}
+      destroyOnClose
+      closable={false}
+      maskClosable={false}
+      width="80%"
+      // placement={'bottom'}
+    >
+      <div
+        style={{
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'space-between'
+        }}
+      >
+        <h3>质量任务</h3>
+        <div>
+          <Button onClick={onCancel} style={{ marginRight: 6 }}>
+            取 消
+          </Button>
+          <Button disabled={loading} type="primary" onClick={handleSave}>
+            保 存
+          </Button>
+        </div>
+      </div>
+      <div style={{ margin: '10px 0', display: 'flex', alignItems: 'center' }}>
+        <label>质量任务名称:</label>
+        <Input
+          value={taskName}
+          onChange={(e) => setTaskName(e.target.value)}
+          style={{ width: 300 }}
+        />
+      </div>
+      <div style={{ margin: '10px 0', display: 'flex', alignItems: 'center' }}>
+        <label>元数据:</label>
+        <Button onClick={() => setMetaVisible(true)}>选择元数据</Button>
+        <div
+          style={{
+            margin: '0 10px',
+            display: 'flex',
+            alignItems: 'center',
+            flexWrap: 'wrap',
+            flex: 1
+          }}
+        >
+          <Tag closable>Tag 2</Tag>
+          <Tag closable>Tag 2</Tag>
+          <Tag closable>Tag 2</Tag>
+          <Tag closable>Tag 2</Tag>
+          <Tag closable>Tag 2</Tag>
+          <Tag closable>Tag 2</Tag>
+        </div>
+      </div>
+      <div>
+        <p style={{ margin: '10px 0' }}>字段映射</p>
+        <Table columns={paramsColumns} />
+      </div>
+      <MetadataModal
+        visible={metaVisible}
+        onCancel={() => setMetaVisible(false)}
+      />
+    </Drawer>
+  )
+}
+
+export default QualityTaskFormModal

+ 149 - 0
app/containers/DataGovernanceQualityAudit/components/ScheduleFormModal.tsx

@@ -0,0 +1,149 @@
+import React from 'react'
+import {
+  Modal,
+  Form,
+  Radio,
+  Button,
+  Input,
+  Switch,
+  Select,
+  DatePicker,
+  InputNumber
+} from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IClassification, ISchedule } from '../types'
+
+const FormItem = Form.Item
+
+interface IScheduleFormModalProps extends FormComponentProps<ISchedule> {
+  visible: boolean
+  loading: boolean
+  formView: ISchedule
+  onSave: (view: ISchedule) => void
+  onCancel: () => void
+}
+
+class ScheduleFormModal extends React.PureComponent<IScheduleFormModalProps> {
+  // @ts-ignore
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, formView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const copyView: ISchedule = { ...fieldsValue }
+      onSave(copyView)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render() {
+    const { form, visible, loading, formView, onCancel } = this.props
+    const { getFieldDecorator } = form
+
+    const modalButtons = [
+      <Button key={'back'} size={'large'} onClick={onCancel}>
+        取 消
+      </Button>,
+      <Button
+        disabled={loading}
+        key={'submit'}
+        size={'large'}
+        type={'primary'}
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    ]
+
+    return (
+      <Modal
+        title={'调度器'}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+        destroyOnClose
+        // width={800}
+      >
+        <Form>
+          <FormItem label={'调度周期'} {...this.formItemStyle} required>
+            {getFieldDecorator<ISchedule>('periodUnit', {
+              initialValue: formView?.periodUnit,
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(
+              <Radio.Group>
+                <Radio value={'Minute'}>分钟</Radio>
+                <Radio value={'Hour'}>小时</Radio>
+                <Radio value={'Day'}>日</Radio>
+                <Radio value={'Week'}>周</Radio>
+                <Radio value={'Month'}>月</Radio>
+                <Radio value={'Year'}>年</Radio>
+              </Radio.Group>
+            )}
+          </FormItem>
+          <FormItem label={'调度间隔'} {...this.formItemStyle} required>
+            {getFieldDecorator<ISchedule>('name', {
+              initialValue: formView?.name,
+              rules: [{ required: true, message: '请输入' }]
+            })(<InputNumber />)}
+          </FormItem>
+
+          <FormItem label={'起止日期'} {...this.formItemStyle} required>
+            {getFieldDecorator<ISchedule>('name', {
+              initialValue: [formView?.startDate, formView.endDate],
+              rules: [
+                {
+                  required: true,
+                  message: '请输入'
+                }
+              ]
+            })(
+              <DatePicker.RangePicker
+                style={{ width: '100%' }}
+                format={'YYYY-MM-DD hh:mm:ss'}
+              />
+            )}
+          </FormItem>
+
+          <FormItem label={'是否激活'} {...this.formItemStyle} required>
+            {getFieldDecorator<ISchedule>('name', {
+              initialValue: formView?.jobStatus === 'started',
+              rules: [
+                {
+                  required: true,
+                  message: '请选择'
+                }
+              ]
+            })(
+              <Switch
+                checkedChildren="开"
+                unCheckedChildren="关"
+                defaultChecked
+              />
+            )}
+          </FormItem>
+        </Form>
+        <p>
+          提示:您希望每 执行一次该任务, 从 时间开始到 时间停止周期调度,
+          并且当前任务状态 ${}
+        </p>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<IScheduleFormModalProps>()(ScheduleFormModal)

+ 333 - 2
app/containers/DataGovernanceQualityAudit/index.tsx

@@ -1,13 +1,344 @@
-import React from 'react'
+import React, { useEffect, useState } from 'react'
 import Container, { ContainerBody } from 'components/Container'
 import Helmet from 'react-helmet'
+import Box from 'components/Box'
+import styles from 'containers/DataManagerView/index.less'
+import {
+  Button,
+  Divider,
+  Dropdown,
+  Icon,
+  Menu,
+  message,
+  Popconfirm,
+  Spin,
+  Table
+} from 'antd'
+import classnames from 'classnames'
+import request from 'utils/request'
+import api from 'utils/api'
+import { ColumnProps } from 'antd/lib/table'
+import {
+  IClassification,
+  IQualityTask
+} from 'containers/DataGovernanceQualityAudit/types'
+import ClassificationsFormModal from 'containers/DataGovernanceQualityAudit/components/ClassificationsFormModal'
+import QualityTaskFormModal from 'containers/DataGovernanceQualityAudit/components/QualityTaskFormModal'
+import ScheduleFormModal from 'containers/DataGovernanceQualityAudit/components/ScheduleFormModal'
+import header from 'containers/Display/Editor/Header'
 
 export default function DataGovernanceQualityAudit() {
+  const [tableLoading, setTableLoading] = useState(false)
+
+  const [treeLoading, setTreeLoading] = useState(false)
+  const [selectedKey, setSelectedKey] = useState<number>()
+
+  const [qualityTasks, setQualityTasks] = useState<IQualityTask[]>([])
+  const [qtVisible, setQtVisible] = useState(false)
+  const [qtLoading, setQtLoading] = useState(false)
+  // eslint-disable-next-line no-undef
+  const [qtForm, setQtForm] = useState<Partial<IQualityTask>>({})
+
+  const [classifications, setClassifications] = useState<IClassification[]>([])
+  const [cfVisible, setCfVisible] = useState(false)
+  const [cfLoading, setCfLoading] = useState(false)
+  const [cfForm, setCfForm] = useState<IClassification>({})
+
+  const [scVisible, setScVisible] = useState(false)
+  const [scLoading, setSCLoading] = useState(false)
+  const [scForm, setSCForm] = useState<IClassification>({})
+
+  const tableColumns: Array<ColumnProps<IQualityTask>> = [
+    {
+      title: '任务名称',
+      dataIndex: 'taskName'
+    },
+    {
+      title: '元数据名称',
+      dataIndex: 'metadataName'
+    },
+    {
+      title: '稽核字段个数',
+      dataIndex: 'standardName'
+    },
+    {
+      title: '操作',
+      render: (_, data) => (
+        <>
+          <a
+            onClick={() => {
+              setQtVisible(true)
+              setQtForm(data)
+            }}
+          >
+            编辑
+          </a>
+          <Divider type="vertical" />
+          <Dropdown
+            overlay={
+              <Menu>
+                <Menu.Item key="0" onClick={() => setScVisible(true)}>
+                  设置调度
+                </Menu.Item>
+                <Menu.Item key="1">
+                  <Popconfirm
+                    title="确定立即稽查吗?"
+                    placement="bottom"
+                    onConfirm={() => handleSetDispatchRightNow(data)}
+                  >
+                    <a>立即稽查</a>
+                  </Popconfirm>
+                </Menu.Item>
+                <Menu.Item key="2">修改</Menu.Item>
+                <Menu.Item key="3">
+                  <Popconfirm
+                    title="确定删除?"
+                    placement="bottom"
+                    // onConfirm
+                  >
+                    <a>删除</a>
+                  </Popconfirm>
+                </Menu.Item>
+              </Menu>
+            }
+          >
+            <a>
+              {' '}
+              更多 <Icon type="down" />
+            </a>
+          </Dropdown>
+        </>
+      )
+    }
+  ]
+
+  const handleEditTreeItem = (form: IClassification) => {
+    setCfForm(form)
+    setCfVisible(true)
+  }
+
+  const handleDeleteTreeItem = async (c: IClassification) => {
+    try {
+      setTreeLoading(true)
+      const data = await request(`${api.deleteAuditClassification}${c.id}`, {
+        method: 'DELETE'
+      })
+      // @ts-ignore
+      if (data?.header?.code === 200) {
+        message.success({ content: '删除成功' })
+        await queryClassifications()
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        data?.header?.msg && message.error({ content: data?.header?.msg })
+      }
+    } finally {
+      setTreeLoading(false)
+    }
+  }
+
+  const renderTree = (catalogues: IClassification[]) => (
+    <>
+      {catalogues.map((c, idx) => (
+        <div
+          key={c.id ?? idx}
+          className={classnames(styles.treeNode, {
+            [styles.treeNodeSelected]: selectedKey === c.id
+          })}
+        >
+          <span
+            className={styles.treeNodeLeft}
+            onClick={() => {
+              setSelectedKey(c.id)
+            }}
+          >
+            <Icon type="file" />
+            {c.name}
+          </span>
+          <Dropdown
+            overlay={() => (
+              <Menu>
+                <Menu.Item key="0" onClick={() => handleEditTreeItem(c)}>
+                  编辑
+                </Menu.Item>
+                <Menu.Item key="1">
+                  <Popconfirm
+                    title="确定删除?"
+                    placement="bottom"
+                    onConfirm={() => handleDeleteTreeItem(c)}
+                  >
+                    <a>删除</a>
+                  </Popconfirm>
+                </Menu.Item>
+              </Menu>
+            )}
+            trigger={['click']}
+          >
+            <Icon type="more" />
+          </Dropdown>
+        </div>
+      ))}
+    </>
+  )
+
+  const handleSetDispatchRightNow = async (data: IQualityTask) => {
+    try {
+      setTableLoading(true)
+      const result = await request(
+        `${api.setDispatchRightNow}${data.cronJobId}`,
+        {
+          method: 'PUT'
+        }
+      )
+      // @ts-ignore
+      if (result.header.code === 200) {
+        message.success({ content: '立即稽核完成' })
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        result?.header?.msg && message.success({ content: result.header.msg })
+      }
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const queryClassifications = async () => {
+    try {
+      setTreeLoading(true)
+      const data = await request(api.getAuditClassification, { method: 'GET' })
+      // @ts-ignore
+      setClassifications(data?.payload ?? [])
+      // @ts-ignore
+      setSelectedKey(data?.payload?.[0]?.id)
+    } finally {
+      setTreeLoading(false)
+    }
+  }
+
+  const queryQualityTasks = async () => {
+    try {
+      setTableLoading(true)
+      const data = await request(`${api.getQualityTask}?pId=${selectedKey}`, {
+        method: 'GET'
+      })
+      // @ts-ignore
+      setQualityTasks(data?.payload ?? [])
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const handleSaveCfForm = async (form: IClassification) => {
+    try {
+      setCfLoading(true)
+      const url = cfForm.id
+        ? api.updateAuditClassification + cfForm.id
+        : api.createAuditClassification
+      const result = await request(url, {
+        method: cfForm.id ? 'PUT' : 'POST',
+        data: { ...cfForm, ...form }
+      })
+      // @ts-ignore
+      if (result?.header?.code === 200) {
+        setCfVisible(false)
+        await queryClassifications()
+      }
+    } finally {
+      setCfLoading(false)
+    }
+  }
+
+  useEffect(() => {
+    queryClassifications()
+  }, [])
+
+  useEffect(() => {
+    if (selectedKey) {
+      queryQualityTasks()
+    }
+  }, [selectedKey])
+
   return (
     <Container>
       <Helmet title="质量稽核" />
+      <ContainerBody>
+        <Box>
+          <Box.Header>
+            <Box.Title>质量稽核</Box.Title>
+          </Box.Header>
+          <Box.Body>
+            <div className={styles.treeTableContainer}>
+              <div className={styles.treeContainer}>
+                <div className={styles.treeTitle}>
+                  <h6>数据质量</h6>
+                  <div
+                    className={styles.treePlusNode}
+                    onClick={() => {
+                      setCfForm({})
+                      setCfVisible(true)
+                    }}
+                  >
+                    <Icon type="plus" />
+                  </div>
+                </div>
+                <div className={styles.treeContent}>
+                  <Spin spinning={treeLoading}>
+                    {renderTree(classifications)}
+                  </Spin>
+                </div>
+              </div>
 
-      <ContainerBody>质量稽核</ContainerBody>
+              <div style={{ flex: 1 }}>
+                <div style={{ padding: '0 0 20px' }}>
+                  <Button
+                    type="primary"
+                    icon="plus"
+                    onClick={() => {
+                      setQtVisible(true)
+                      setQtForm(null)
+                    }}
+                  >
+                    新增
+                  </Button>
+                </div>
+                <Table
+                  style={{ flex: 1 }}
+                  bordered
+                  rowKey="id"
+                  loading={tableLoading}
+                  dataSource={qualityTasks}
+                  columns={tableColumns}
+                  pagination={false}
+                  // onChange={this.tableChange}
+                />
+              </div>
+            </div>
+            <br />
+          </Box.Body>
+        </Box>
+      </ContainerBody>
+      <ClassificationsFormModal
+        visible={cfVisible}
+        formView={cfForm}
+        onSave={handleSaveCfForm}
+        loading={cfLoading}
+        onCancel={() => setCfVisible(false)}
+      />
+      <QualityTaskFormModal
+        visible={qtVisible}
+        formView={qtForm}
+        onSave={handleSaveCfForm}
+        loading={qtLoading}
+        onCancel={() => setQtVisible(false)}
+      />
+      <ScheduleFormModal
+        visible={scVisible}
+        loading={scLoading}
+        formView={scForm}
+        onSave={undefined}
+        onCancel={() => setScVisible(false)}
+      />
     </Container>
   )
 }

+ 42 - 0
app/containers/DataGovernanceQualityAudit/types.ts

@@ -0,0 +1,42 @@
+export interface IClassification {
+  id?: number
+  name?: string
+}
+
+export interface IQualityTask {
+  cronJobId: 6
+  endTime: string
+  id: number
+  metadataConfig: string
+  metadataName: string
+  pId: number
+  startTime: string
+  taskName: string
+  typeName: string
+  viewId: number
+}
+
+export interface ISchedule {
+  // 非必须 分钟
+  minute?: number
+  // 非必须 名称
+  name?: string
+  // 非必须 任务类型 auditor
+  jobType?: string
+  // 非必须 new 不启用、 started 启用
+  jobStatus?: string
+  // 非必须 描述
+  description?: string
+  // 非必须 Minute:分钟、Hour小时、Day 日、Week周、Month 月、Year 年
+  periodUnit?: string
+  // 非必须 小时
+  hour?: number
+  // 非必须 0 */10 * * * ?
+  cronExpression?: string
+  // 非必须 开始时间
+  startDate?: string
+  // 非必须 结束时间
+  endDate?: string
+  // 非必须项目id
+  projectId?: number
+}

+ 128 - 0
app/containers/DataManagerDictionary/components/DictDataFormModal.tsx

@@ -0,0 +1,128 @@
+/*
+ * <<
+ * Davinci
+ * ==
+ * Copyright (C) 2016 - 2017 EDP
+ * ==
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * >>
+ */
+
+import React from 'react'
+import { Modal, Form, Button, Input, Switch } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IDictData } from '../types'
+
+const FormItem = Form.Item
+
+interface IDictTypeFormModalProps extends FormComponentProps<IDictData> {
+  visible: boolean
+  loading: boolean
+  fromView: IDictData
+  onSave: (view: IDictData) => void
+  onCancel: () => void
+}
+
+export class DictTypeFormModal extends React.PureComponent<IDictTypeFormModalProps> {
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, fromView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const copyView: IDictData = { ...fieldsValue }
+      onSave(copyView)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render() {
+    const { form, visible, loading, fromView, onCancel } = this.props
+    const { getFieldDecorator } = form
+
+    const modalButtons = [
+      <Button key="back" size="large" onClick={onCancel}>
+        取 消
+      </Button>,
+      <Button
+        disabled={loading}
+        key="submit"
+        size="large"
+        type="primary"
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    ]
+
+    return (
+      <Modal
+        title={fromView ? '编辑' : '新增'}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+        destroyOnClose
+      >
+        <Form>
+          {/*<FormItem label='字典编码' {...this.formItemStyle}>*/}
+          {/*  {getFieldDecorator<IDictData>('dictCode', {*/}
+          {/*    validateFirst: true,*/}
+          {/*    rules: [*/}
+          {/*      { required: true, message: '不能为空' }*/}
+          {/*    ],*/}
+          {/*    initialValue: fromView?.dictCode*/}
+          {/*  })(<Input />)}*/}
+          {/*</FormItem>*/}
+          <FormItem label="字典标签" {...this.formItemStyle}>
+            {getFieldDecorator<IDictData>('dictLabel', {
+              initialValue: fromView?.dictLabel
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="字典键值" {...this.formItemStyle}>
+            {getFieldDecorator<IDictData>('dictValue', {
+              initialValue: fromView?.dictValue
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="状态" {...this.formItemStyle}>
+            {getFieldDecorator<IDictData>('status', {
+              valuePropName: 'checked',
+              initialValue: !fromView ? true : fromView?.status === 0
+            })(
+              <Switch
+                checkedChildren="正常"
+                unCheckedChildren="停用"
+                defaultChecked
+              />
+            )}
+          </FormItem>
+          <FormItem label="备注" {...this.formItemStyle}>
+            {getFieldDecorator<IDictData>('remark', {
+              initialValue: fromView?.remark
+            })(<Input.TextArea />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<IDictTypeFormModalProps>()(DictTypeFormModal)

+ 157 - 47
app/containers/DataManagerDictionary/components/DictDatasModal.tsx

@@ -19,13 +19,22 @@
  */
 
 import React, { useEffect, useState } from 'react'
-import { Modal, Form, Button, Input, Col, Table, Row, Divider } from 'antd'
-import { FormComponentProps } from 'antd/lib/form'
-import { ICatalogue, IDictData, IViewBase } from '../types'
+import {
+  Modal,
+  Button,
+  Col,
+  Table,
+  Row,
+  Divider,
+  Popconfirm,
+  message
+} from 'antd'
+import { IDictData } from '../types'
 import { ColumnProps } from 'antd/lib/table'
 import ButtonGroup from 'antd/es/button/button-group'
 import api from 'utils/api'
 import request from 'utils/request'
+import DictDataFormModal from 'containers/DataManagerDictionary/components/DictDataFormModal'
 
 interface IDictDatasModalProps {
   visible: boolean
@@ -34,25 +43,14 @@ interface IDictDatasModalProps {
 }
 
 const DictDatasModal = (props: IDictDatasModalProps) => {
-
   const { visible, onCancel, dictType } = props
 
   const [tableLoading, setTableLoading] = useState(false)
   const [dictDatas, setDictDatas] = useState<IDictData[]>([])
 
-  const queryDictDatas = async() => {
-    if (!dictType) {
-      return
-    }
-    try {
-      setTableLoading(true)
-      const data = await request(api.dictDatas + `?dictType=${dictType}`, { method: 'GET' })
-      setDictDatas(data?.payload ?? [])
-    } finally {
-      setTableLoading(false)
-    }
-  }
-
+  const [ddFormVisible, setDdFormVisible] = useState(false)
+  const [ddFormLoading, setDdFormLoading] = useState(false)
+  const [ddFormView, setDdFormView] = useState<IDictData>({})
 
   const tableColumns: Array<ColumnProps<IDictData>> = [
     {
@@ -76,8 +74,20 @@ const DictDatasModal = (props: IDictDatasModalProps) => {
       dataIndex: 'status',
       render: (text) => (
         <>
-          {text === 0 && <span style={{ padding: '2px 4px', background: 'green', color: '#fff' }}>正常</span>}
-          {text === 1 && <span style={{ padding: '2px 4px', background: 'red', color: '#fff' }}>停用</span>}
+          {text === 0 && (
+            <span
+              style={{ padding: '2px 4px', background: 'green', color: '#fff' }}
+            >
+              正常
+            </span>
+          )}
+          {(text === 1 || text === '') && (
+            <span
+              style={{ padding: '2px 4px', background: 'red', color: '#fff' }}
+            >
+              停用
+            </span>
+          )}
         </>
       )
     },
@@ -91,46 +101,146 @@ const DictDatasModal = (props: IDictDatasModalProps) => {
     },
     {
       title: '操作',
-      render: () => {
+      render: (_, data) => {
         return (
           <>
-            <a>编辑</a>
-            <Divider type='vertical' />
-            <a>删除</a>
+            <a
+              onClick={() => {
+                setDdFormVisible(true)
+                setDdFormView(data)
+              }}
+            >
+              编辑
+            </a>
+            <Divider type="vertical" />
+            <Popconfirm
+              title="确定删除?"
+              placement="bottom"
+              onConfirm={() => handleDeleteDictData(data)}
+            >
+              <a>删除</a>
+            </Popconfirm>
           </>
         )
       }
     }
   ]
+
+  const queryDictDatas = async () => {
+    if (!dictType) {
+      return
+    }
+    try {
+      setTableLoading(true)
+      const data = await request(api.dictDatas + `?dictType=${dictType}`, {
+        method: 'GET'
+      })
+      // @ts-ignore
+      setDictDatas(data?.payload ?? [])
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const handleDeleteDictData = async (data: IDictData) => {
+    try {
+      setTableLoading(true)
+      const result = await request(`${api.deleteDictData}${data.dictCode}`, {
+        method: 'delete'
+      })
+      // @ts-ignore
+      if (result.header.code === 200) {
+        message.success({ content: '删除成功' })
+        queryDictDatas()
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        result.header.msg && message.success({ content: result.header.msg })
+      }
+    } finally {
+      setTableLoading(false)
+    }
+  }
+
+  const handleSaveDDForm = async (form: IDictData) => {
+    try {
+      setDdFormLoading(true)
+      const url = ddFormView?.dictCode
+        ? api.updateDictData + ddFormView.dictCode
+        : api.createDictData
+
+      const result = await request(url, {
+        method: ddFormView?.dictCode ? 'PUT' : 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        data: { ...ddFormView, dictType, ...form, status: form.status ? 0 : 1 }
+      })
+      // @ts-ignore
+      if (result.header.code === 200) {
+        message.success({
+          content: (ddFormView?.dictCode ? '修改' : '新增') + '成功'
+        })
+        setDdFormVisible(false)
+        queryDictDatas()
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        result.header.msg && message.success({ content: result.header.msg })
+      }
+    } finally {
+      setDdFormLoading(false)
+    }
+  }
+
   useEffect(() => {
     queryDictDatas()
   }, [dictType])
 
   return (
-    <Modal
-      title='数据字典数据'
-      // wrapClassName='ant-modal-small'
-      visible={visible}
-      footer={null}
-      onCancel={onCancel}
-      destroyOnClose
-      width={'80%'}
-    >
-      <Row>
-        <Col span={24}>
-          <Table
-            bordered
-            rowKey='dictCode'
-            loading={tableLoading}
-            dataSource={dictDatas}
-            columns={tableColumns}
-            pagination={false}
-            // onChange={this.tableChange}
-          />
-        </Col>
-      </Row>
-    </Modal>
+    <>
+      <Modal
+        title="数据字典数据"
+        // wrapClassName='ant-modal-small'
+        visible={visible}
+        footer={null}
+        onCancel={onCancel}
+        destroyOnClose
+        width={'80%'}
+      >
+        <div style={{ padding: '10px 0' }}>
+          <Button
+            onClick={() => {
+              setDdFormVisible(true)
+              setDdFormView({})
+            }}
+          >
+            新增
+          </Button>
+        </div>
+        <Row>
+          <Col span={24}>
+            <Table
+              bordered
+              rowKey="dictCode"
+              loading={tableLoading}
+              dataSource={dictDatas}
+              columns={tableColumns}
+              pagination={false}
+              // onChange={this.tableChange}
+            />
+          </Col>
+        </Row>
+      </Modal>
+      <DictDataFormModal
+        visible={ddFormVisible}
+        fromView={ddFormView}
+        onSave={handleSaveDDForm}
+        onCancel={() => setDdFormVisible(false)}
+        loading={ddFormLoading}
+      />
+    </>
   )
 }
 
-export default (DictDatasModal)
+export default DictDatasModal

+ 35 - 17
app/containers/DataManagerDictionary/index.tsx

@@ -2,7 +2,17 @@ import React, { useEffect, useState } from 'react'
 import Helmet from 'react-helmet'
 import Container, { ContainerBody } from 'components/Container'
 import Box from 'components/Box'
-import { Button, Col, Divider, Icon, Popconfirm, Row, Table, Tooltip } from 'antd'
+import {
+  Button,
+  Col,
+  Divider,
+  Icon,
+  message,
+  Popconfirm,
+  Row,
+  Table,
+  Tooltip
+} from 'antd'
 import { ColumnProps } from 'antd/lib/table'
 import { compose } from 'redux'
 import request from 'utils/request'
@@ -77,7 +87,7 @@ function DataDictionary() {
           >
             编辑
           </a>
-          <Divider type='vertical' />
+          <Divider type="vertical" />
           <a
             onClick={() => {
               setDictType(data.dictType)
@@ -87,10 +97,10 @@ function DataDictionary() {
             列表
           </a>
 
-          <Divider type='vertical' />
+          <Divider type="vertical" />
           <Popconfirm
-            title='确定删除?'
-            placement='bottom'
+            title="确定删除?"
+            placement="bottom"
             onConfirm={() => handleDeleteDictType(data.dictId)}
           >
             <a>删除</a>
@@ -100,16 +110,17 @@ function DataDictionary() {
     }
   ]
 
-  const queryDictTypes = async() => {
+  const queryDictTypes = async () => {
     try {
       setTableLoading(true)
       const data = await request(api.dictTypes, { method: 'GET' })
+      // @ts-ignore
       setDictTypes(data?.payload ?? [])
     } finally {
       setTableLoading(false)
     }
   }
-  const handleDeleteDictType = async(dictId) => {
+  const handleDeleteDictType = async (dictId) => {
     try {
       setTableLoading(true)
       await request(`${api.deleteDictType}/${dictId}`, { method: 'delete' })
@@ -118,19 +129,26 @@ function DataDictionary() {
       setTableLoading(false)
     }
   }
-  const handleSaveDictType = async(dt: IDictType) => {
+  const handleSaveDictType = async (dt: IDictType) => {
     // // 0=正常,1=停用
     try {
       setDictTypeFormLoading(true)
       const url = dictTypeForm?.dictId
         ? `${api.updateDictType}/${dictTypeForm.dictId}`
         : api.createDictType
-      await request(url, {
+      const result = await request(url, {
         method: dictTypeForm?.dictId ? 'PUT' : 'POST',
         data: { ...(dictTypeForm ?? {}), ...dt, status: dt.status ? 0 : 1 }
       })
-      setDictTypeFormVisible(false)
-      queryDictTypes()
+      // @ts-ignore
+      if (result.header.code === 200) {
+        setDictTypeFormVisible(false)
+        queryDictTypes()
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        result.header.msg && message.error({ content: result.header.msg })
+      }
     } finally {
       setDictTypeFormLoading(false)
     }
@@ -141,20 +159,20 @@ function DataDictionary() {
   }, [])
   return (
     <Container>
-      <Helmet title='数据字典' />
+      <Helmet title="数据字典" />
 
       <ContainerBody>
         <Box>
           <Box.Header>
             <Box.Title>
-              <Icon type='bars' />
+              <Icon type="bars" />
               数据字典列表
             </Box.Title>
             <Box.Tools>
-              <Tooltip placement='bottom' title='新增'>
+              <Tooltip placement="bottom" title="新增">
                 <Button
-                  type='primary'
-                  icon='plus'
+                  type="primary"
+                  icon="plus"
                   onClick={() => {
                     setDictTypeFormVisible(true)
                     setDictTypeForm(null)
@@ -168,7 +186,7 @@ function DataDictionary() {
               <Col span={24}>
                 <Table
                   bordered
-                  rowKey='dictId'
+                  rowKey="dictId"
                   loading={tableLoading}
                   dataSource={dictTypes}
                   columns={tableColumns}

+ 1 - 3
app/containers/DataManagerDictionary/types.ts

@@ -1,5 +1,3 @@
-
-
 export interface IDictType {
   dictId?: number
   dictName?: string
@@ -18,5 +16,5 @@ export interface IDictData {
   isDefault?: string
   listClass?: string
   remark?: string
-  status?: string
+  status?: number
 }

+ 18 - 17
app/containers/DataManagerView/components/CatalogueModal.tsx

@@ -29,7 +29,11 @@ interface ICopyModalProps extends FormComponentProps<ICatalogue> {
   visible: boolean
   loading: boolean
   fromView: ICatalogue
-  onCheckUniqueName: (viewName: string, resolve: () => void, reject: (err: string) => void) => void
+  onCheckUniqueName: (
+    viewName: string,
+    resolve: () => void,
+    reject: (err: string) => void
+  ) => void
   onSave: (view: ICatalogue) => void
   onCancel: () => void
 }
@@ -53,11 +57,15 @@ export class CatalogueModal extends React.PureComponent<ICopyModalProps> {
 
   private checkName = (_, value, callback) => {
     const { onCheckUniqueName } = this.props
-    onCheckUniqueName(value, () => {
-      callback()
-    }, (err) => {
-      callback(err)
-    })
+    onCheckUniqueName(
+      value,
+      () => {
+        callback()
+      },
+      (err) => {
+        callback(err)
+      }
+    )
   }
 
   private clearFieldsValue = () => {
@@ -65,20 +73,13 @@ export class CatalogueModal extends React.PureComponent<ICopyModalProps> {
   }
 
   public render() {
-    console.log(this.props)
     const { form, visible, loading, fromView, onCancel } = this.props
     const { getFieldDecorator } = form
-    // if (!fromView) { return null }
 
-    const modalButtons = [(
-      <Button
-        key="back"
-        size="large"
-        onClick={onCancel}
-      >
+    const modalButtons = [
+      <Button key="back" size="large" onClick={onCancel}>
         取 消
-      </Button>
-    ), (
+      </Button>,
       <Button
         disabled={loading}
         key="submit"
@@ -88,7 +89,7 @@ export class CatalogueModal extends React.PureComponent<ICopyModalProps> {
       >
         保 存
       </Button>
-    )]
+    ]
 
     return (
       <Modal

+ 8 - 8
app/containers/DataManagerView/index.less

@@ -1,4 +1,4 @@
-@import "~assets/less/variable";
+@import '~assets/less/variable';
 
 .treeTableContainer {
   display: flex;
@@ -7,34 +7,34 @@
 .treeContainer {
   padding: 0 0 10px;
   border: 1px solid rgb(232, 232, 232);
-  margin-bottom: 20px;
   width: 240px;
   margin-right: 10px;
   border-radius: 4px 4px 0 0;
 
-
   .treeTitle {
     display: flex;
     justify-content: space-between;
     align-items: center;
     line-height: 36px;
     padding: 10px 10px;
-    background-color: rgb(250, 250, 250);;
+    background-color: rgb(250, 250, 250);
     border-bottom: 1px solid #e8e8e8;
 
-    h6, > div {
+    h6,
+    > div {
       line-height: 21px;
     }
 
     .treePlusNode {
       //padding: 0 4px;
-      cursor: pointer
+      cursor: pointer;
     }
   }
 
   .treeContent {
-    width: 100%;
+    //width: 100%;
     padding: 10px 0 0;
+    margin: 0 10px;
 
     .treeNode {
       padding: 8px 10px;
@@ -42,7 +42,7 @@
       display: flex;
       justify-content: space-between;
       cursor: pointer;
-      transition: all .2s;
+      transition: all 0.2s;
       border-radius: 4px;
 
       &.treeNodeChild {

+ 260 - 168
app/containers/DataManagerView/index.tsx

@@ -34,36 +34,36 @@ import sagas from './sagas'
 
 import { checkNameUniqueAction } from 'containers/App/actions'
 import { ViewActions, ViewActionType } from './actions'
-import { makeSelectViews, makeSelectLoading } from './selectors'
+import { makeSelectLoading, makeSelectViews } from './selectors'
 import { makeSelectCurrentProject } from 'containers/Projects/selectors'
 
 import ModulePermission from '../Account/components/checkModulePermission'
 import { initializePermission } from '../Account/components/checkUtilPermission'
 
 import {
-  Table,
-  Tooltip,
+  Breadcrumb,
   Button,
-  Row,
   Col,
-  Breadcrumb,
+  Dropdown,
   Icon,
-  Popconfirm,
+  Menu,
   message,
-  Tree,
-  Popover,
-  Dropdown,
-  Menu, Modal, Spin
+  Popconfirm,
+  Row,
+  Spin,
+  Table,
+  Tooltip,
+  Tree
 } from 'antd'
 import { ColumnProps, PaginationConfig, SorterResult } from 'antd/lib/table'
 import { ButtonProps } from 'antd/lib/button'
-import Container, { ContainerTitle, ContainerBody } from 'components/Container'
+import Container, { ContainerBody, ContainerTitle } from 'components/Container'
 import Box from 'components/Box'
 import SearchFilterDropdown from 'components/SearchFilterDropdown'
 import CopyModal from './components/CopyModal'
 import CatalogueModal from './components/CatalogueModal'
 
-import { IViewBase, IView, IViewLoading, ICatalogue } from './types'
+import { ICatalogue, IViewBase, IViewLoading } from './types'
 import { IProject } from '../Projects/types'
 
 import utilStyles from 'assets/less/util.less'
@@ -85,7 +85,9 @@ interface IViewListDispatchProps {
   onCheckName: (data, resolve, reject) => void
 }
 
-type IViewListProps = IViewListStateProps & IViewListDispatchProps & RouteComponentWithParams
+type IViewListProps = IViewListStateProps &
+  IViewListDispatchProps &
+  RouteComponentWithParams
 
 // tslint:disable-next-line:interface-name
 interface Catalogue {
@@ -125,8 +127,11 @@ interface IViewListStates {
 
 const { TreeNode, DirectoryTree } = Tree
 
-export class ViewList extends React.PureComponent<IViewListProps, IViewListStates> {
-
+export class ViewList extends React.PureComponent<
+  IViewListProps,
+  IViewListStates
+> {
+  // @ts-ignore
   public state: Readonly<IViewListStates> = {
     screenWidth: document.documentElement.clientWidth,
     tempFilterViewName: '',
@@ -159,14 +164,21 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     window.addEventListener('resize', this.setScreenWidth, false)
   }
 
-  private loadViews = async() => {
+  private loadViews = async () => {
     const { projectId } = this.props.match.params
     if (projectId && this.state.selectedCatalogueKeys.length > 0) {
       const parentId = Number(this.state.selectedCatalogueKeys[0])
       try {
         this.setState({ tableLoading: true })
-        const data = await request(api.getViewsByParentId + `?projectId=${projectId}&parentId=${parentId}`, { method: 'get' })
-        this.setState({ viewList: data.payload as unknown as IViewBase[] ?? [] })
+        const data = await request(
+          api.getViewsByParentId +
+            `?projectId=${projectId}&parentId=${parentId}`,
+          { method: 'get' }
+        )
+        // @ts-ignore
+        this.setState({
+          viewList: (data.payload as unknown as IViewBase[]) ?? []
+        })
       } catch (e) {
         console.log(e)
       } finally {
@@ -183,14 +195,17 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     this.setState({ screenWidth: document.documentElement.clientWidth })
   }
 
-  private getFilterViews = memoizeOne((viewName: string, views: IViewBase[]) => {
-    if (!Array.isArray(views) || !views.length) {
-      return []
+  private getFilterViews = memoizeOne(
+    (viewName: string, views: IViewBase[]) => {
+      if (!Array.isArray(views) || !views.length) {
+        return []
+      }
+      const regex = new RegExp(viewName, 'gi')
+      return views.filter(
+        (v) => v.name.match(regex) || v.description.match(regex)
+      )
     }
-    const regex = new RegExp(viewName, 'gi')
-    const filterViews = views.filter((v) => v.name.match(regex) || v.description.match(regex))
-    return filterViews
-  })
+  )
 
   private static getViewPermission = memoizeOne((project: IProject) => ({
     viewPermission: initializePermission(project, 'viewPermission'),
@@ -198,49 +213,67 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     EditButton: ModulePermission<ButtonProps>(project, 'view', false)(Button)
   }))
 
-  private getTableColumns = (
-    { viewPermission, AdminButton, EditButton }: ReturnType<typeof ViewList.getViewPermission>
-  ) => {
+  private getTableColumns = ({
+    viewPermission,
+    AdminButton,
+    EditButton
+  }: ReturnType<typeof ViewList.getViewPermission>) => {
     // const { views } = this.props
     const { viewList } = this.state
-    const { tempFilterViewName, filterViewName, filterDropdownVisible, tableSorter } = this.state
+    const {
+      tempFilterViewName,
+      filterViewName,
+      filterDropdownVisible,
+      tableSorter
+    } = this.state
     const sourceNames = viewList.map(({ sourceName }) => sourceName)
 
-    const columns: Array<ColumnProps<IViewBase>> = [{
-      title: '名称',
-      dataIndex: 'name',
-      filterDropdown: (
-        <SearchFilterDropdown
-          placeholder='名称'
-          value={tempFilterViewName}
-          onChange={this.filterViewNameChange}
-          onSearch={this.searchView}
-        />
-      ),
-      filterDropdownVisible,
-      onFilterDropdownVisibleChange: (visible: boolean) => this.setState({ filterDropdownVisible: visible }),
-      sorter: (a, b) => (a.name > b.name ? 1 : -1),
-      sortOrder: tableSorter && tableSorter.columnKey === 'name' ? tableSorter.order : void 0
-    }, {
-      title: '描述',
-      dataIndex: 'description'
-    }, {
-      title: '数据源',
-      // title: 'Source',
-      dataIndex: 'sourceName',
-      filterMultiple: false,
-      onFilter: (val, record) => record.sourceName === val,
-      filters: sourceNames
-        .filter((name, idx) => sourceNames.indexOf(name) === idx)
-        .map((name) => ({ text: name, value: name }))
-    }]
+    const columns: Array<ColumnProps<IViewBase>> = [
+      {
+        title: '名称',
+        dataIndex: 'name',
+        filterDropdown: (
+          <SearchFilterDropdown
+            placeholder="名称"
+            value={tempFilterViewName}
+            onChange={this.filterViewNameChange}
+            onSearch={this.searchView}
+          />
+        ),
+        filterDropdownVisible,
+        onFilterDropdownVisibleChange: (visible: boolean) =>
+          this.setState({ filterDropdownVisible: visible }),
+        sorter: (a, b) => (a.name > b.name ? 1 : -1),
+        sortOrder:
+          tableSorter && tableSorter.columnKey === 'name'
+            ? tableSorter.order
+            : void 0
+      },
+      {
+        title: '描述',
+        dataIndex: 'description'
+      },
+      {
+        title: '数据源',
+        // title: 'Source',
+        dataIndex: 'sourceName',
+        filterMultiple: false,
+        onFilter: (val, record) => record.sourceName === val,
+        filters: sourceNames
+          .filter((name, idx) => sourceNames.indexOf(name) === idx)
+          .map((name) => ({ text: name, value: name }))
+      }
+    ]
 
     if (filterViewName) {
       const regex = new RegExp(`(${filterViewName})`, 'gi')
       columns[0].render = (text: string) => (
         <span
           dangerouslySetInnerHTML={{
-            __html: text.replace(regex, `<span class='${utilStyles.highlight}'>$1</span>`)
+            __html: text.replace(
+              regex,
+              `<span class='${utilStyles.highlight}'>$1</span>`
+            )
           }}
         />
       )
@@ -252,20 +285,30 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
         width: 150,
         className: utilStyles.textAlignCenter,
         render: (_, record) => (
-          <span className='ant-table-action-column'>
-            <Tooltip title='复制'>
-              <EditButton icon='copy' shape='circle' type='ghost' onClick={this.copyView(record)} />
+          <span className="ant-table-action-column">
+            <Tooltip title="复制">
+              <EditButton
+                icon="copy"
+                shape="circle"
+                type="ghost"
+                onClick={this.copyView(record)}
+              />
             </Tooltip>
-            <Tooltip title='修改'>
-              <EditButton icon='edit' shape='circle' type='ghost' onClick={this.editView(record.id)} />
+            <Tooltip title="修改">
+              <EditButton
+                icon="edit"
+                shape="circle"
+                type="ghost"
+                onClick={this.editView(record.id)}
+              />
             </Tooltip>
             <Popconfirm
-              title='确定删除?'
-              placement='bottom'
+              title="确定删除?"
+              placement="bottom"
               onConfirm={this.deleteView(record.id)}
             >
-              <Tooltip title='删除'>
-                <AdminButton icon='delete' shape='circle' type='ghost' />
+              <Tooltip title="删除">
+                <AdminButton icon="delete" shape="circle" type="ghost" />
               </Tooltip>
             </Popconfirm>
           </span>
@@ -302,7 +345,11 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
 
   private addView = () => {
     const { history, match } = this.props
-    history.push(`/project/${match.params.projectId}/view`)
+    history.push(
+      `/project/${
+        match.params.projectId
+      }/view?parentId=${this.state.selectedCatalogueKeys?.join()}`
+    )
   }
 
   private copyView = (fromView: IViewBase) => () => {
@@ -338,20 +385,31 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     })
   }
 
-  private checkViewUniqueName = (viewName: string, resolve: () => void, reject: (err: string) => void) => {
+  private checkViewUniqueName = (
+    viewName: string,
+    resolve: () => void,
+    reject: (err: string) => void
+  ) => {
     const { currentProject, onCheckName } = this.props
-    onCheckName({ name: viewName, projectId: currentProject.id }, resolve, reject)
+    onCheckName(
+      { name: viewName, projectId: currentProject.id },
+      resolve,
+      reject
+    )
   }
 
-  private getCatalogues = async() => {
+  private getCatalogues = async () => {
     try {
       const { projectId } = this.props.match.params
 
       this.setState({ treeLoading: true })
       // @ts-ignore
-      const { payload } = await request(api.getCatalogues + `?projectId=${projectId}`, { method: 'GET' })
+      const { payload } = await request(
+        api.getCatalogues + `?projectId=${projectId}`,
+        { method: 'GET' }
+      )
       this.setState({
-        catalogues: (payload as unknown as ICatalogue[]),
+        catalogues: payload as unknown as ICatalogue[],
         selectedCatalogueKeys: payload?.[0]?.id ? [`${payload?.[0]?.id}`] : []
       })
     } catch (e) {
@@ -361,7 +419,7 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     }
   }
 
-  private handleSaveCatalogue = async(catalogue: ICatalogue) => {
+  private handleSaveCatalogue = async (catalogue: ICatalogue) => {
     const { projectId } = this.props.match.params
     if (!projectId) {
       return
@@ -372,11 +430,19 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
       this.setState({ saveCatalogueLoading: true })
       // const api = this.state.catalogueFromView ? api.updateCatalogue : api.createCatalogue
       if (catalogueFromView) {
-        await request(api.updateCatalogue + `/${catalogueFromView.id}`, { method: 'PUT' })
+        await request(api.updateCatalogue + `/${catalogueFromView.id}`, {
+          method: 'PUT'
+        })
       } else {
-        await request(api.createCatalogue, { method: 'post', data: { ...catalogue, parentId, projectId } })
+        await request(api.createCatalogue, {
+          method: 'post',
+          data: { ...catalogue, parentId, projectId }
+        })
       }
-      this.setState({ saveCatalogueLoading: true, catalogueModalVisible: false })
+      this.setState({
+        saveCatalogueLoading: true,
+        catalogueModalVisible: false
+      })
       this.getCatalogues()
     } finally {
       this.setState({
@@ -385,129 +451,153 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
       })
     }
   }
+  private handleEditCatalogue = (c: ICatalogue) => {
+    this.setState({
+      catalogueModalVisible: true,
+      catalogueFromView: c
+    })
+  }
+
+  private handleDeleteCatalogue = async (c: ICatalogue) => {
+    try {
+      this.setState({ treeLoading: true })
+      const data = await request(api.deleteCatalogue + `/${c.id}`, {
+        method: 'DELETE'
+      })
+      // @ts-ignore
+      if (data?.header?.code === 200) {
+        message.error({ content: '删除成功' })
+        this.getCatalogues()
+      } else {
+        // @ts-ignore
+        // tslint:disable-next-line:no-unused-expression
+        data?.header?.msg && message.error({ content: data?.header?.msg })
+      }
+    } finally {
+      this.setState({ treeLoading: false })
+    }
+  }
 
   private renderTree = (catalogues: ICatalogue[]) => {
     const { selectedCatalogueKeys } = this.state
     // tslint:disable-next-line:jsx-wrap-multiline
-    return <>
-      {
-        catalogues.map((c, idx) => (
-          <>
+    return (
+      <>
+        {catalogues.map((c, idx) => (
+          <React.Fragment key={c.id ?? idx}>
             <div
               key={c.id ?? idx}
               className={classnames(styles.treeNode, {
-                [styles.treeNodeSelected]: selectedCatalogueKeys.includes(`${c.id}`),
+                [styles.treeNodeSelected]: selectedCatalogueKeys.includes(
+                  `${c.id}`
+                ),
                 [styles.treeNodeChild]: !!c.parentId
               })}
             >
-          <span
-            className={styles.treeNodeLeft}
-            onClick={() => {
-              this.setState({ selectedCatalogueKeys: [`${c.id}`] }, () => {
-                console.log(this.state.selectedCatalogueKeys, '~~~')
-                this.loadViews()
-              })
-            }}
-          >
-            <Icon type='folder-open' />
-            {c.name}</span>
-              <span>
-             <Dropdown
-               overlay={() => (
-                 <Menu>
-                   <Menu.Item
-                     key='0'
-                     onClick={() => {
-                       this.setState({
-                         catalogueModalVisible: true,
-                         catalogueFromView: c
-                       })
-                     }}
-                   >编辑</Menu.Item>
-                   <Menu.Item
-                     key='1'
-                     onClick={() => {
-                       Modal.confirm({
-                         title: '确认删除吗?',
-                         onOk: async() => {
-                           try {
-                             const data = await request(api.deleteCatalogue + `/${c.id}`, { method: 'DELETE' })
-                             console.log(data)
-
-                             if (data?.header?.code === 200) {
-                               this.getCatalogues()
-                             }
-                           } catch (e) {
-
-                           }
-                         }
-                       })
-                     }}
-                   >
-                     删除
-                   </Menu.Item>
-                 </Menu>
-               )}
-               trigger={['click']}
-             >
-                <Icon type='more' />
-            </Dropdown>
-           </span>
+              <span
+                className={styles.treeNodeLeft}
+                onClick={() => {
+                  this.setState({ selectedCatalogueKeys: [`${c.id}`] }, () => {
+                    this.loadViews()
+                  })
+                }}
+              >
+                <Icon type="folder-open" />
+                {c.name}
+              </span>
+              <Dropdown
+                overlay={() => (
+                  <Menu>
+                    <Menu.Item
+                      key="0"
+                      onClick={() => this.handleEditCatalogue(c)}
+                    >
+                      编辑
+                    </Menu.Item>
+                    <Menu.Item key="1">
+                      <Popconfirm
+                        title="确定删除?"
+                        placement="bottom"
+                        onConfirm={() => this.handleDeleteCatalogue(c)}
+                      >
+                        <a> 删除</a>
+                      </Popconfirm>
+                    </Menu.Item>
+                  </Menu>
+                )}
+                trigger={['click']}
+              >
+                <Icon type="more" />
+              </Dropdown>
             </div>
             <div style={{ marginLeft: 20 }}>
               {c.children && this.renderTree(c.children)}
             </div>
-          </>
-        ))
-      }
-    </>
+          </React.Fragment>
+        ))}
+      </>
+    )
   }
 
   public render() {
     const { currentProject, views, loading } = this.props
     const { screenWidth, filterViewName, viewList } = this.state
-    const { viewPermission, AdminButton, EditButton } = ViewList.getViewPermission(currentProject)
-    const tableColumns = this.getTableColumns({ viewPermission, AdminButton, EditButton })
+    const { viewPermission, AdminButton, EditButton } =
+      ViewList.getViewPermission(currentProject)
+    const tableColumns = this.getTableColumns({
+      viewPermission,
+      AdminButton,
+      EditButton
+    })
     const tablePagination: PaginationConfig = {
       ...this.basePagination,
       simple: screenWidth <= 768
     }
     const filterViews = this.getFilterViews(filterViewName, viewList)
 
-    const { copyModalVisible, copyFromView, catalogueModalVisible, catalogueFromView } = this.state
+    const {
+      copyModalVisible,
+      copyFromView,
+      catalogueModalVisible,
+      catalogueFromView
+    } = this.state
 
     const pathname = this.props.history.location.pathname
 
     return (
       <>
         <Container>
-          <Helmet title='数据资产' />
-          {
-            !pathname.includes('dataManager') && <ContainerTitle>
+          <Helmet title="数据资产" />
+          {!pathname.includes('dataManager') && (
+            <ContainerTitle>
               <Row>
                 <Col span={24} className={utilStyles.shortcut}>
                   <Breadcrumb className={utilStyles.breadcrumb}>
                     <Breadcrumb.Item>
-                      <Link to=''>View</Link>
+                      <Link to="">View</Link>
                     </Breadcrumb.Item>
                   </Breadcrumb>
                   <Link to={`/account/organization/${currentProject.orgId}`}>
-                    <i className='iconfont icon-organization' />
+                    <i className="iconfont icon-organization" />
                   </Link>
                 </Col>
               </Row>
             </ContainerTitle>
-          }
+          )}
           <ContainerBody>
             <Box>
               <Box.Header>
                 <Box.Title>
-                  <Icon type='bars' />
+                  <Icon type="bars" />
                   数据资产列表
                 </Box.Title>
                 <Box.Tools>
-                  <Tooltip placement='bottom' title='新增'>
-                    <AdminButton type='primary' icon='plus' onClick={this.addView} />
+                  <Tooltip placement="bottom" title="新增">
+                    <AdminButton
+                      type="primary"
+                      icon="plus"
+                      onClick={this.addView}
+                    />
                   </Tooltip>
                 </Box.Tools>
               </Box.Header>
@@ -518,13 +608,11 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
                       <h6>资源目录列表</h6>
                       <div
                         className={styles.treePlusNode}
-                        onClick={
-                          () => {
-                            this.setState({ catalogueModalVisible: true })
-                          }
-                        }
+                        onClick={() => {
+                          this.setState({ catalogueModalVisible: true })
+                        }}
                       >
-                        <Icon type='plus' />
+                        <Icon type="plus" />
                       </div>
                     </div>
                     <div className={styles.treeContent}>
@@ -536,7 +624,7 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
                   <Table
                     style={{ flex: 1 }}
                     bordered
-                    rowKey='id'
+                    rowKey="id"
                     loading={this.state.tableLoading}
                     dataSource={filterViews}
                     columns={tableColumns}
@@ -544,6 +632,7 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
                     onChange={this.tableChange}
                   />
                 </div>
+                <div style={{ padding: 20 }} />
               </Box.Body>
             </Box>
           </ContainerBody>
@@ -567,14 +656,17 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
       </>
     )
   }
-
 }
 
 const mapDispatchToProps = (dispatch: Dispatch<ViewActionType>) => ({
-  onLoadViews: (projectId, parentId) => dispatch(ViewActions.loadViews(projectId, parentId)),
-  onDeleteView: (viewId, resolve) => dispatch(ViewActions.deleteView(viewId, resolve)),
+  onLoadViews: (projectId, parentId) =>
+    dispatch(ViewActions.loadViews(projectId, parentId)),
+  onDeleteView: (viewId, resolve) =>
+    dispatch(ViewActions.deleteView(viewId, resolve)),
   onCopyView: (view, resolve) => dispatch(ViewActions.copyView(view, resolve)),
-  onCheckName: (data, resolve, reject) => dispatch(checkNameUniqueAction('view', data, resolve, reject))
+  // @ts-ignore
+  onCheckName: (data, resolve, reject) =>
+    dispatch(checkNameUniqueAction('view', data, resolve, reject))
 })
 
 const mapStateToProps = createStructuredSelector({
@@ -583,12 +675,12 @@ const mapStateToProps = createStructuredSelector({
   loading: makeSelectLoading()
 })
 
-const withConnect = connect<IViewListStateProps, IViewListDispatchProps, RouteComponentWithParams>(mapStateToProps, mapDispatchToProps)
+const withConnect = connect<
+  IViewListStateProps,
+  IViewListDispatchProps,
+  RouteComponentWithParams
+>(mapStateToProps, mapDispatchToProps)
 const withReducer = injectReducer({ key: 'view', reducer })
 const withSaga = injectSaga({ key: 'view', saga: sagas })
 
-export default compose(
-  withReducer,
-  withSaga,
-  withConnect
-)(ViewList)
+export default compose(withReducer, withSaga, withConnect)(ViewList)

+ 187 - 100
app/containers/View/Editor.tsx

@@ -38,7 +38,10 @@ import { RouteComponentWithParams } from 'utils/types'
 import { hideNavigator } from '../App/actions'
 import { ViewActions, ViewActionType } from './actions'
 import { SourceActions, SourceActionType } from 'containers/Source/actions'
-import { OrganizationActions, OrganizationActionType } from 'containers/Organizations/actions'
+import {
+  OrganizationActions,
+  OrganizationActionType
+} from 'containers/Organizations/actions'
 
 import {
   makeSelectEditingView,
@@ -49,7 +52,6 @@ import {
   makeSelectSqlLimit,
   makeSelectSqlValidation,
   makeSelectLoading,
-
   makeSelectChannels,
   makeSelectTenants,
   makeSelectBizs,
@@ -59,9 +61,19 @@ import {
 import { makeSelectProjectRoles } from 'containers/Projects/selectors'
 
 import {
-  IView, IViewModel, IViewRoleRaw, IViewRole, IViewVariable, IViewInfo,
-  IExecuteSqlParams, IExecuteSqlResponse, IViewLoading, ISqlValidation,
-  IDacChannel, IDacTenant, IDacBiz
+  IView,
+  IViewModel,
+  IViewRoleRaw,
+  IViewRole,
+  IViewVariable,
+  IViewInfo,
+  IExecuteSqlParams,
+  IExecuteSqlResponse,
+  IViewLoading,
+  ISqlValidation,
+  IDacChannel,
+  IDacTenant,
+  IDacBiz
 } from './types'
 import { ISource, ISchema } from '../Source/types'
 import { ViewVariableTypes } from './constants'
@@ -78,6 +90,7 @@ import ViewVariableList from './components/ViewVariableList'
 import VariableModal from './components/VariableModal'
 
 import Styles from './View.less'
+import { querystring } from '../../../share/util'
 
 interface IViewEditorStateProps {
   editingView: IView
@@ -92,7 +105,7 @@ interface IViewEditorStateProps {
 
   channels: IDacChannel[]
   tenants: IDacTenant[]
-  bizs: IDacBiz[],
+  bizs: IDacBiz[]
 
   isLastExecuteWholeSql: boolean
 }
@@ -108,7 +121,11 @@ interface IViewEditorDispatchProps {
   onLoadSources: (projectId: number) => void
   onLoadSourceDatabases: (sourceId: number) => void
   onLoadDatabaseTables: (sourceId: number, databaseName: string) => void
-  onLoadTableColumns: (sourceId: number, databaseName: string, tableName: string) => void
+  onLoadTableColumns: (
+    sourceId: number,
+    databaseName: string,
+    tableName: string
+  ) => void
   onExecuteSql: (params: IExecuteSqlParams, exeType: EExecuteType) => void
   onAddView: (view: IView, resolve: () => void) => void
   onEditView: (view: IView, resolve: () => void) => void
@@ -116,16 +133,18 @@ interface IViewEditorDispatchProps {
   onUpdateEditingViewInfo: (viewInfo: IViewInfo) => void
   onSetSqlLimit: (limit: number) => void
 
-  onLoadDacChannels: () => void,
-  onLoadDacTenants: (channelName: string) => void,
-  onLoadDacBizs: (channelName: string, tenantId: number) => void,
+  onLoadDacChannels: () => void
+  onLoadDacTenants: (channelName: string) => void
+  onLoadDacBizs: (channelName: string, tenantId: number) => void
 
   onResetState: () => void
   onLoadProjectRoles: (projectId: number) => void
   onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) => void
 }
 
-type IViewEditorProps = IViewEditorStateProps & IViewEditorDispatchProps & RouteComponentWithParams
+type IViewEditorProps = IViewEditorStateProps &
+  IViewEditorDispatchProps &
+  RouteComponentWithParams
 
 interface IViewEditorStates {
   containerHeight: number
@@ -136,9 +155,10 @@ interface IViewEditorStates {
   sqlFragment: string
 }
 
-
-export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorStates> {
-
+export class ViewEditor extends React.Component<
+  IViewEditorProps,
+  IViewEditorStates
+> {
   public state: Readonly<IViewEditorStates> = {
     containerHeight: 0,
     currentStep: 0,
@@ -170,9 +190,10 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
     onLoadDacChannels()
   }
 
-  public static getDerivedStateFromProps:
-    React.GetDerivedStateFromProps<IViewEditorProps, IViewEditorStates>
-    = (props, state) => {
+  public static getDerivedStateFromProps: React.GetDerivedStateFromProps<
+    IViewEditorProps,
+    IViewEditorStates
+  > = (props, state) => {
     const { match, editingView, sqlValidation } = props
     const { viewId } = match.params
     const { init, sqlValidationCode } = state
@@ -181,23 +202,23 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
       notification.destroy()
       sqlValidation.code === 200
         ? notification.success({
-          message: '执行成功',
-          duration: 3
-        })
+            message: '执行成功',
+            duration: 3
+          })
         : notification.error({
-          message: '执行失败',
-          description: (
-            <Tooltip
-              placement='bottom'
-              trigger='click'
-              title={sqlValidation.message}
-              overlayClassName={Styles.errorMessage}
-            >
-              <a>点击查看错误信息</a>
-            </Tooltip>
-          ),
-          duration: null
-        })
+            message: '执行失败',
+            description: (
+              <Tooltip
+                placement="bottom"
+                trigger="click"
+                title={sqlValidation.message}
+                overlayClassName={Styles.errorMessage}
+              >
+                <a>点击查看错误信息</a>
+              </Tooltip>
+            ),
+            duration: null
+          })
       if (sqlValidation.code === 200) {
         lastSuccessExecutedSql = editingView.sql
       }
@@ -230,7 +251,10 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
     ViewEditor.ExecuteSql(this.props, this.state.sqlFragment)
   }
 
-  private static ExecuteSql = (props: IViewEditorProps, sqlFragment?: string) => {
+  private static ExecuteSql = (
+    props: IViewEditorProps,
+    sqlFragment?: string
+  ) => {
     const { onExecuteSql, editingView, editingViewInfo, sqlLimit } = props
     const { sourceId, sql } = editingView
     const { variable } = editingViewInfo
@@ -240,7 +264,8 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
       limit: sqlLimit,
       variables: variable
     }
-    const exeType = sqlFragment == null ? EExecuteType.whole : EExecuteType.single
+    const exeType =
+      sqlFragment == null ? EExecuteType.whole : EExecuteType.single
     onExecuteSql(updatedParams, exeType)
   }
 
@@ -271,14 +296,29 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
   }
 
   private saveView = () => {
-    const { onAddView, onEditView, editingView, editingViewInfo, projectRoles, match } = this.props
+    const searchParams = querystring(
+      this.props.location?.search?.replace('?', '')
+    )
+    const {
+      onAddView,
+      onEditView,
+      editingView,
+      editingViewInfo,
+      projectRoles,
+      match
+    } = this.props
     const { projectId } = match.params
     const { model, variable, roles } = editingViewInfo
     const { id: viewId } = editingView
-    const validRoles = roles.filter(({ roleId }) => projectRoles && projectRoles.findIndex(({ id }) => id === roleId) >= 0)
+    const validRoles = roles.filter(
+      ({ roleId }) =>
+        projectRoles && projectRoles.findIndex(({ id }) => id === roleId) >= 0
+    )
     const updatedView: IView = {
       ...editingView,
       projectId: +projectId,
+      // @ts-ignore
+      parentId: searchParams?.parentId * 1 || null,
       model: JSON.stringify(model),
       variable: JSON.stringify(variable),
       roles: validRoles.map<IViewRoleRaw>(({ roleId, columnAuth, rowAuth }) => {
@@ -288,7 +328,7 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
           if (!v) {
             return false
           }
-          return (v.type === ViewVariableTypes.Authorization && !v.fromService)
+          return v.type === ViewVariableTypes.Authorization && !v.fromService
         })
         return {
           roleId,
@@ -297,7 +337,9 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
         }
       })
     }
-    viewId ? onEditView(updatedView, this.goToViewList) : onAddView(updatedView, this.goToViewList)
+    viewId
+      ? onEditView(updatedView, this.goToViewList)
+      : onAddView(updatedView, this.goToViewList)
   }
 
   private goToViewList = () => {
@@ -305,7 +347,8 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
     const { projectId } = match.params
     const prefix = window.localStorage.getItem('inDataService') ?? ''
     const prefixPath = prefix ? '/' + prefix : prefix
-    history.push(`/project/${projectId}${prefixPath}/views`)
+    history.replace(`/project/${projectId}${prefixPath}/views`)
+    window.location.reload()
   }
 
   private viewChange = (propName: keyof IView, value: string | number) => {
@@ -368,68 +411,85 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
     onUpdateEditingViewInfo(updatedViewInfo)
   }
 
-  private getSqlHints = memoizeOne((sourceId: number, schema: ISchema, variables: IViewVariable[]) => {
-    if (!sourceId) {
-      return {}
-    }
-
-    const variableHints = variables.reduce((acc, v) => {
-      acc[`$${v.name}$`] = []
-      return acc
-    }, {})
-    const { mapDatabases, mapTables, mapColumns } = schema
-    if (!mapDatabases[sourceId]) {
-      return {}
-    }
+  private getSqlHints = memoizeOne(
+    (sourceId: number, schema: ISchema, variables: IViewVariable[]) => {
+      if (!sourceId) {
+        return {}
+      }
 
-    const tableHints: { [tableName: string]: string[] } = Object.values(mapTables).reduce((acc, tablesInfo) => {
-      if (tablesInfo.sourceId !== sourceId) {
+      const variableHints = variables.reduce((acc, v) => {
+        acc[`$${v.name}$`] = []
         return acc
+      }, {})
+      const { mapDatabases, mapTables, mapColumns } = schema
+      if (!mapDatabases[sourceId]) {
+        return {}
       }
 
-      tablesInfo.tables.forEach(({ name: tableName }) => {
-        acc[tableName] = []
+      const tableHints: { [tableName: string]: string[] } = Object.values(
+        mapTables
+      ).reduce((acc, tablesInfo) => {
+        if (tablesInfo.sourceId !== sourceId) {
+          return acc
+        }
+
+        tablesInfo.tables.forEach(({ name: tableName }) => {
+          acc[tableName] = []
+        })
+        return acc
+      }, {})
+
+      Object.values(mapColumns).forEach((columnsInfo) => {
+        if (columnsInfo.sourceId !== sourceId) {
+          return
+        }
+        const { tableName, columns } = columnsInfo
+        if (tableHints[tableName]) {
+          tableHints[tableName] = tableHints[tableName].concat(
+            columns.map((col) => col.name)
+          )
+        }
       })
-      return acc
-    }, {})
 
-    Object.values(mapColumns).forEach((columnsInfo) => {
-      if (columnsInfo.sourceId !== sourceId) {
-        return
-      }
-      const { tableName, columns } = columnsInfo
-      if (tableHints[tableName]) {
-        tableHints[tableName] = tableHints[tableName].concat(columns.map((col) => col.name))
+      const hints = {
+        ...variableHints,
+        ...tableHints
       }
-    })
-
-    const hints = {
-      ...variableHints,
-      ...tableHints
+      return hints
     }
-    return hints
-  })
+  )
 
   public render() {
     const {
-      sources, schema,
-      sqlDataSource, sqlLimit, loading, projectRoles,
-      channels, tenants, bizs,
-      editingView, editingViewInfo,
+      sources,
+      schema,
+      sqlDataSource,
+      sqlLimit,
+      loading,
+      projectRoles,
+      channels,
+      tenants,
+      bizs,
+      editingView,
+      editingViewInfo,
       isLastExecuteWholeSql,
-      onLoadSourceDatabases, onLoadDatabaseTables, onLoadTableColumns, onSetSqlLimit,
-      onLoadDacTenants, onLoadDacBizs
+      onLoadSourceDatabases,
+      onLoadDatabaseTables,
+      onLoadTableColumns,
+      onSetSqlLimit,
+      onLoadDacTenants,
+      onLoadDacBizs
     } = this.props
     const { currentStep, lastSuccessExecutedSql, sqlFragment } = this.state
     const { model, variable, roles: viewRoles } = editingViewInfo
     const sqlHints = this.getSqlHints(editingView.sourceId, schema, variable)
     const containerVisible = !currentStep
     const modelAuthVisible = !!currentStep
-    const nextDisabled = (editingView.sql !== lastSuccessExecutedSql)
+    const nextDisabled = editingView.sql !== lastSuccessExecutedSql
 
     return (
       <>
-        <Helmet title='数据资产' />
+        <Helmet title="数据资产" />
         <div className={Styles.viewEditor}>
           <div className={Styles.header}>
             <div className={Styles.steps}>
@@ -442,7 +502,7 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
             onVariableChange={this.variableChange}
           >
             <SourceTable
-              key='SourceTable'
+              key="SourceTable"
               view={editingView}
               sources={sources}
               schema={schema}
@@ -451,11 +511,22 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
               onDatabaseSelect={onLoadDatabaseTables}
               onTableSelect={onLoadTableColumns}
             />
-            <SqlEditor key='SqlEditor' value={editingView.sql} hints={sqlHints} onSqlChange={this.sqlChange}
-                       onSelect={this.sqlSelect} onCmdEnter={this.executeSql} />
-            <SqlPreview key='SqlPreview' size='small' loading={loading.execute} response={sqlDataSource} />
+            <SqlEditor
+              key="SqlEditor"
+              value={editingView.sql}
+              hints={sqlHints}
+              onSqlChange={this.sqlChange}
+              onSelect={this.sqlSelect}
+              onCmdEnter={this.executeSql}
+            />
+            <SqlPreview
+              key="SqlPreview"
+              size="small"
+              loading={loading.execute}
+              response={sqlDataSource}
+            />
             <EditorBottom
-              key='EditorBottom'
+              key="EditorBottom"
               sqlLimit={sqlLimit}
               loading={loading.execute}
               nextDisabled={nextDisabled}
@@ -465,9 +536,9 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
               onExecuteSql={this.executeSql}
               onStepChange={this.stepChange}
             />
-            <ViewVariableList key='ViewVariableList' variables={variable} />
+            <ViewVariableList key="ViewVariableList" variables={variable} />
             <VariableModal
-              key='VariableModal'
+              key="VariableModal"
               channels={channels}
               tenants={tenants}
               bizs={bizs}
@@ -494,25 +565,35 @@ export class ViewEditor extends React.Component<IViewEditorProps, IViewEditorSta
 
 const mapDispatchToProps = (dispatch) => ({
   onHideNavigator: () => dispatch(hideNavigator()),
-  onLoadViewDetail: (viewId: number) => dispatch(ViewActions.loadViewsDetail([viewId], null, true)),
+  onLoadViewDetail: (viewId: number) =>
+    dispatch(ViewActions.loadViewsDetail([viewId], null, true)),
   onLoadSources: (projectId) => dispatch(SourceActions.loadSources(projectId)),
-  onLoadSourceDatabases: (sourceId) => dispatch(SourceActions.loadSourceDatabases(sourceId)),
-  onLoadDatabaseTables: (sourceId, databaseName) => dispatch(SourceActions.loadDatabaseTables(sourceId, databaseName)),
-  onLoadTableColumns: (sourceId, databaseName, tableName) => dispatch(SourceActions.loadTableColumns(sourceId, databaseName, tableName)),
-  onExecuteSql: (params, exeType?) => dispatch(ViewActions.executeSql(params, exeType)),
-  onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) => dispatch(ViewActions.setIsLastExecuteWholeSql(isLastExecuteWholeSql)),
+  onLoadSourceDatabases: (sourceId) =>
+    dispatch(SourceActions.loadSourceDatabases(sourceId)),
+  onLoadDatabaseTables: (sourceId, databaseName) =>
+    dispatch(SourceActions.loadDatabaseTables(sourceId, databaseName)),
+  onLoadTableColumns: (sourceId, databaseName, tableName) =>
+    dispatch(SourceActions.loadTableColumns(sourceId, databaseName, tableName)),
+  onExecuteSql: (params, exeType?) =>
+    dispatch(ViewActions.executeSql(params, exeType)),
+  onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) =>
+    dispatch(ViewActions.setIsLastExecuteWholeSql(isLastExecuteWholeSql)),
   onAddView: (view, resolve) => dispatch(ViewActions.addView(view, resolve)),
   onEditView: (view, resolve) => dispatch(ViewActions.editView(view, resolve)),
   onUpdateEditingView: (view) => dispatch(ViewActions.updateEditingView(view)),
-  onUpdateEditingViewInfo: (viewInfo: IViewInfo) => dispatch(ViewActions.updateEditingViewInfo(viewInfo)),
+  onUpdateEditingViewInfo: (viewInfo: IViewInfo) =>
+    dispatch(ViewActions.updateEditingViewInfo(viewInfo)),
   onSetSqlLimit: (limit: number) => dispatch(ViewActions.setSqlLimit(limit)),
 
   onLoadDacChannels: () => dispatch(ViewActions.loadDacChannels()),
-  onLoadDacTenants: (channelName) => dispatch(ViewActions.loadDacTenants(channelName)),
-  onLoadDacBizs: (channelName, tenantId) => dispatch(ViewActions.loadDacBizs(channelName, tenantId)),
+  onLoadDacTenants: (channelName) =>
+    dispatch(ViewActions.loadDacTenants(channelName)),
+  onLoadDacBizs: (channelName, tenantId) =>
+    dispatch(ViewActions.loadDacBizs(channelName, tenantId)),
 
   onResetState: () => dispatch(ViewActions.resetViewState()),
-  onLoadProjectRoles: (projectId) => dispatch(OrganizationActions.loadProjectRoles(projectId))
+  onLoadProjectRoles: (projectId) =>
+    dispatch(OrganizationActions.loadProjectRoles(projectId))
 })
 
 const mapStateToProps = createStructuredSelector({
@@ -536,9 +617,15 @@ const mapStateToProps = createStructuredSelector({
 const withConnect = connect(mapStateToProps, mapDispatchToProps)
 const withReducer = injectReducer({ key: 'view', reducer })
 const withSaga = injectSaga({ key: 'view', saga: sagas })
-const withReducerSource = injectReducer({ key: 'source', reducer: reducerSource })
+const withReducerSource = injectReducer({
+  key: 'source',
+  reducer: reducerSource
+})
 const withSagaSource = injectSaga({ key: 'source', saga: sagasSource })
-const withReducerProject = injectReducer({ key: 'project', reducer: reducerProject })
+const withReducerProject = injectReducer({
+  key: 'project',
+  reducer: reducerProject
+})
 const withSagaProject = injectSaga({ key: 'project', saga: sagasProject })
 
 export default compose(

+ 49 - 1
app/utils/api.ts

@@ -65,6 +65,12 @@ export default {
 
   // 查询数据字典数据 /api/v3/dict/dictDatas?dictType=xx
   dictDatas: `${API_HOST}/dict/dictDatas`,
+  // PUT /api/v3/dict/updateDictData/{id}
+  updateDictData: `${API_HOST}/dict/updateDictData/`,
+  // POST /api/v3/dict/createDictData
+  createDictData: `${API_HOST}/dict/createDictData/`,
+  // DELETE  /api/v3/dict/deleteDictData/{id}
+  deleteDictData: `${API_HOST}/dict/deleteDictData/`,
 
   // 获取数据字典类型 /api/v3/dict/dictTypes
   dictTypes: `${API_HOST}/dict/dictTypes`,
@@ -74,6 +80,48 @@ export default {
   updateDictType: `${API_HOST}/dict/updateDictType/`,
   // 删除数据字典类型 /api/v3/dict/deleteDictType/{id} DELETE
   deleteDictType: `${API_HOST}/dict/deleteDictType/`,
-  // getCatalogues: ``,
 
+  // 从字典表中获取主题列表 /api/v3/dict/getDictDataList/{dictType}
+  getSubjects: `${API_HOST}/dict/getDictDataList/`,
+
+  // 查询数据标准
+  getDataSubject: `${API_HOST}/dataSubject/getDataSubject`,
+  // 删除数据标准 /api/v3/dataSubject/deleteDataSubject/{id}
+  deleteDataSubject: `${API_HOST}/dataSubject/deleteDataSubject/`,
+  // /api/v3/dataSubject/updateDataSubject/{id}
+  updateDataSubject: `${API_HOST}/dataSubject/updateDataSubject/`,
+  createDataSubject: `${API_HOST}/dataSubject/createDataSubject`,
+
+  getDataRules: `${API_HOST}/dataRules/getDataRules`,
+  createDataRules: `${API_HOST}/dataRules/createDataRules`,
+  // /api/v3/dataRules/updateDataRules/1 put
+  updateDataRules: `${API_HOST}/dataRules/updateDataRules/`,
+  // /api/v3/dataRules/deleteDataRules/1 delete
+  deleteDataRules: `${API_HOST}/dataRules/deleteDataRules/`,
+
+  // 查询稽核类型
+  getAuditClassification: `${API_HOST}/auditClassification/getAuditClassification`,
+  createAuditClassification: `${API_HOST}/auditClassification/createAuditClassification`,
+  // /api/v3/auditClassification/updateAuditClassification/{id} PUT
+  updateAuditClassification: `${API_HOST}/auditClassification/updateAuditClassification/`,
+  // /api/v3/auditClassification/deleteAuditClassification/{id} DELETE
+  deleteAuditClassification: `${API_HOST}/auditClassification/deleteAuditClassification/`,
+
+  getQualityTask: `${API_HOST}/qualityTask/getQualityTask`,
+  // 新增质量任务 POST /api/v3/qualityTask/createQualityTask
+  createQualityTask: `${API_HOST}/qualityTask/createQualityTask`,
+  // 修改质量任务 PUT /api/v3/qualityTask/updateQualityTask/{id}
+  updateQualityTask: `${API_HOST}/qualityTask/updateQualityTask/`,
+  // 删除质量任务 DELETE /api/v3/qualityTask/deleteQualityTask/{id}
+  deleteQualityTask: `${API_HOST}/qualityTask/deleteQualityTask/`,
+
+  // 设置调度 PUT /api/v3/qualityTask/setDispatch/{id}
+  setDispatch: `${API_HOST}/qualityTask/setDispatch/`,
+  // 立即稽核 PUT /api/v3/qualityTask/auditor/{id}
+  setDispatchRightNow: `${API_HOST}/qualityTask/auditor/`,
+
+  // 质量报告列表 GET /api/v3/qualityTask/qualityReport
+  qualityReport: `${API_HOST}/qualityTask/qualityReport`,
+  // 质量报告详情 GET /api/v3/qualityTask/qualityReportDetail/{taskId}
+  qualityReportDetail: `${API_HOST}/qualityTask/qualityReportDetail/`
 }