Jelajahi Sumber

冲突合并

Xiao_123 1 bulan lalu
induk
melakukan
b8b65c0bab
70 mengubah file dengan 5452 tambahan dan 348 penghapusan
  1. 1 0
      package.json
  2. 206 0
      src/api/dataChart.js
  3. 49 2
      src/api/dataGovernance.js
  4. 5 0
      src/api/index.js
  5. 91 0
      src/charts/initChart.vue
  6. 6 2
      src/charts/lowCode.vue
  7. 5 5
      src/components/Dialog/index.vue
  8. 98 0
      src/components/FullscreenToggle/index.vue
  9. 3 3
      src/components/List/table.vue
  10. 0 116
      src/components/MForm/index.vue
  11. 181 0
      src/components/MSearch/index.vue
  12. 215 54
      src/router/routes.js
  13. 4 2
      src/store/modules/user.js
  14. 4 0
      src/utils/index.js
  15. 5 4
      src/utils/request.js
  16. 29 25
      src/views/dataBook/components/mGraph.vue
  17. 7 1
      src/views/dataBook/components/mGraphList.vue
  18. 1 1
      src/views/dataBook/dataMetaData/components/metaDataInfo.vue
  19. 1 1
      src/views/dataBook/dataModel/details.vue
  20. 0 17
      src/views/dataBook/dataModel/dynamic/metaData.vue
  21. 78 0
      src/views/dataBook/dataModel/dynamic/metadata.vue
  22. 15 0
      src/views/dataBook/dataProcess/details.vue
  23. 94 3
      src/views/dataBook/dataProcess/index.vue
  24. 2 2
      src/views/dataBook/dataResource/dynamic/metadata.vue
  25. 3 15
      src/views/dataBook/dataResource/index.vue
  26. 86 0
      src/views/dataChart/dataChartConversation.vue
  27. 204 0
      src/views/dataChart/dataChartEdit.vue
  28. 477 0
      src/views/dataChart/dataChartEditChat.vue
  29. 53 0
      src/views/dataChart/index.vue
  30. 106 0
      src/views/dataChart/utils/options.js
  31. 1 1
      src/views/dataDiagram/myChartDiagram/index.vue
  32. 3 0
      src/views/dataFactory/components/graph.vue
  33. 1 1
      src/views/dataFactory/productionLineMonitor/index.vue
  34. 6 4
      src/views/dataGovernance/dataIndicator/components/edit.vue
  35. 26 3
      src/views/dataGovernance/dataIndicator/components/editBase.vue
  36. 1 1
      src/views/dataGovernance/dataIndicator/index.vue
  37. 90 12
      src/views/dataGovernance/dataModules/components/edit.vue
  38. 32 4
      src/views/dataGovernance/dataModules/components/editBase.vue
  39. 171 0
      src/views/dataGovernance/dataModules/components/editMetadataAdd.vue
  40. 1 1
      src/views/dataGovernance/dataModules/index.vue
  41. 189 26
      src/views/dataGovernance/dataProcess/components/edit.vue
  42. 191 0
      src/views/dataGovernance/dataProcess/components/editBase.vue
  43. 54 15
      src/views/dataGovernance/dataProcess/index.vue
  44. 0 1
      src/views/dataGovernance/dataResource/components/Structure/index.vue
  45. 27 4
      src/views/dataGovernance/dataResource/components/editBase.vue
  46. 1 1
      src/views/dataGovernance/dataResource/index.vue
  47. 57 16
      src/views/dataGovernance/metadata/components/edit.vue
  48. 0 3
      src/views/dataGovernance/metadata/index.vue
  49. 1 1
      src/views/dataOrigin/unstructuredData/manualCollection/components/imageImportEdit.vue
  50. 0 1
      src/views/home/components/featureModule.vue
  51. 64 0
      src/views/modelSystem/backup/backupCreate.vue
  52. 72 0
      src/views/modelSystem/backup/backupDetails.vue
  53. 91 0
      src/views/modelSystem/backup/backupDetailsConfirm.vue
  54. 135 0
      src/views/modelSystem/backup/index.vue
  55. 370 0
      src/views/modelSystem/modelHistory/index.vue
  56. 49 0
      src/views/modelSystem/modelHistory/modelDetails.vue
  57. 173 0
      src/views/modelSystem/modelQa/index.vue
  58. 83 0
      src/views/modelSystem/modelQa/modelQaEdit.vue
  59. 84 0
      src/views/modelSystem/modelStatistics/index.vue
  60. 186 0
      src/views/modelSystem/modelTrain/index.vue
  61. 82 0
      src/views/modelSystem/modelTrain/modelTrainEdit.vue
  62. 151 0
      src/views/modelSystem/modelTrain/modelTrainFiles.vue
  63. 83 0
      src/views/modelSystem/modelTrain/modelTrainLog.vue
  64. 184 0
      src/views/modelSystem/modelTrain/modelTrainStatus.vue
  65. 111 0
      src/views/modelSystem/modelTrain/modelTrainTable.vue
  66. 63 0
      src/views/modelSystem/modelTrain/modelTrainTableSubmit.vue
  67. 254 0
      src/views/modelSystem/modelTrainManage/index.vue
  68. 98 0
      src/views/modelSystem/modelTrainManage/modelTrainManageCombine.vue
  69. 147 0
      src/views/modelSystem/modelTrainManage/modelTrainManageEdit.vue
  70. 91 0
      src/views/modelSystem/modelTrainManage/modelTrainManageUpload.vue

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "axios": "^1.7.2",
     "core-js": "^3.8.3",
     "echarts": "^5.4.3",
+    "element-resize-detector": "^1.2.4",
     "exceljs": "^4.4.0",
     "file-saver": "^2.0.1",
     "fs": "0.0.1-security",

+ 206 - 0
src/api/dataChart.js

@@ -0,0 +1,206 @@
+
+import http from '@/utils/request'
+
+// vanna 问答
+export function getAsk (data, config) {
+  return http.post('/vanna/v0/ask_agent', data, config)
+}
+
+// 提交到正确训练集
+export function submitTrainingCorrect (data) {
+  return http.post('/vanna/v0/citu_train_question_sql', data)
+}
+
+// 提交到错误训练集
+export function submitTrainingError (data) {
+  return http.post('/vanna/v0/training_error_question_sql', data)
+}
+
+// 查询反馈记录(支持分页、筛选、排序)
+export function getFeedbackList (data) {
+  return http.post('/vanna/v0/qa_feedback/query', data)
+}
+
+// 删除指定反馈记录
+export function deleteFeedback (feedbackId) {
+  return http.del(`/vanna/v0/qa_feedback/delete/${feedbackId}`)
+}
+
+// 修改指定反馈记录
+export function updateFeedback (feedbackId, data) {
+  return http.put(`/vanna/v0/qa_feedback/update/${feedbackId}`, data)
+}
+
+// 批量添加到训练集
+export function addFeedbackToTraining (data) {
+  return http.post('/vanna/v0/qa_feedback/add_to_training', data)
+}
+
+// 创建新的反馈记录
+export function addFeedback (data) {
+  return http.post('/vanna/v0/qa_feedback/add', data)
+}
+
+// 获取反馈统计信息
+export function getFeedbackStats (data) {
+  return http.get('/vanna/v0/qa_feedback/stats', data)
+}
+
+// 获取指定用户user_id的最近N次的会话信息
+export function getConversations (data) {
+  return http.get('/vanna/v0/user/guest/conversations', data)
+}
+
+// 获取指定用户user_id的最近N次的会话信息
+export function getConversationsById (id) {
+  return http.get(`/vanna/v0/conversation/${id}/messages`)
+}
+
+// 统计Redis中的全部的对话信息
+export function getConversationsStatus () {
+  return http.get('/vanna/v0/conversation_stats')
+}
+
+// 手工删除Redis中全部对话缓存
+export function clearConversation (data) {
+  return http.post('/vanna/v0/conversation_cleanup', data)
+}
+
+// 统计Redis中question embedding缓存
+export function getEmbeddingStats () {
+  return http.get('/vanna/v0/embedding_cache_stats')
+}
+
+// 清理Redis中的question embedding 缓存
+export function clearEmbeddingCache () {
+  return http.post('/vanna/v0/embedding_cache_cleanup')
+}
+
+// 获取训练数据统计信息
+export function getTrainingData () {
+  return http.get('/vanna/v0/training_data/stats')
+}
+
+// 分页查询训练数据,支持筛选和搜索
+export function getTrainingDataList (param) {
+  return http.post('/vanna/v0/training_data/query', param)
+}
+
+// 创建训练数据,支持单条和批量操作
+export function createTrainingData (param) {
+  return http.post('/vanna/v0/training_data/create', param)
+}
+
+// 更新训练数据
+export function updateTrainingData (param) {
+  return http.post('/vanna/v0/training_data/update', param)
+}
+
+// 导入训练数据
+export function uploadTrainingData (param) {
+  return http.upload('/vanna/v0/training_data/upload', param)
+}
+
+// 合并训练数据
+export function combineTrainData (param) {
+  return http.post('/vanna/v0/training_data/combine', param)
+}
+
+// 删除训练数据,支持批量操作
+export function deleteTrainingData (param) {
+  return http.post('/vanna/v0/training_data/delete', param)
+}
+
+// **************** 数据集自动生成和加载 ****************
+
+// 数据集自动生成和加载 创建数据训练任务
+export function createDataTasks (param) {
+  return http.post('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// 数据集自动生成和加载 执行数据训练任务
+export function executeDataTasks (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/execute`, param)
+}
+
+// 数据集自动生成和加载 获取任务状态
+export function getDataTasksStatus (taskId, param) {
+  return http.get(`/vanna/v0/data_pipeline/tasks/${taskId}`, param)
+}
+
+// 数据集自动生成和加载 获取任务列表
+export function getDataTasksList (param) {
+  return http.get('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// 数据集自动生成和加载 删除任务(批量)
+export function deleteDataTasks (param) {
+  return http.del('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// ***************** 表名清单管理 *****************
+
+// 表名清单管理 查询数据库表列表
+export function getDataBaseList (param) {
+  return http.post('/vanna/v0/database/tables', param)
+}
+
+// 表名清单管理 在线提交表名列表
+export function saveDataBase (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/table-list`, param)
+}
+// 表名清单管理 上传表清单文件
+export function uploadDataTasksList (taskId, param) {
+  return http.upload(`/vanna/v0/data_pipeline/tasks/${taskId}/upload-table-list`, param)
+}
+// 表名清单管理 获取表清单文件信息
+export function getDataTasksFileList (param) {
+  return http.get('/vanna/v0/data_pipeline/tasks/{task_id}/table-list-info', param)
+}
+
+// *************** 训练数据文件管理 ****************
+
+// 训练数据文件管理 查看任务文件列表
+export function getTasksFileList (taskId, param) {
+  return http.get(`/vanna/v0/data_pipeline/tasks/${taskId}/files`, param)
+}
+
+// 训练数据文件管理 下载任务文件
+export function downloadTasksFileList (taskId, fileName) {
+  return http.getDownload(`/vanna/v0/data_pipeline/tasks/${taskId}/files/${fileName}`)
+}
+
+// 表名清单管理 上传文件到任务目录
+export function uploadTasksList (taskId, param) {
+  return http.upload(`/vanna/v0/data_pipeline/tasks/${taskId}/files`, param)
+}
+
+// 表名清单管理 日志
+export function getTasksLog (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/logs/query`, param)
+}
+
+// PgVector 表备份
+export function addPgBackup (param) {
+  return http.post('/vanna/v0/data_pipeline/vector/backup', param)
+}
+
+// PgVector 已备份
+export function getPgBackupList (param) {
+  return http.get('/vanna/v0/data_pipeline/vector/restore/list', param)
+}
+
+// PgVector 备份恢复
+export function pgBackupRestore (param) {
+  return http.post('/vanna/v0/data_pipeline/vector/restore', param)
+}
+
+// Checkpoint 统计信息
+export function getCheckpoint (param) {
+  return http.get('/vanna/v0/checkpoint/direct/stats', param)
+}
+
+// 清理Checkpoint
+export function clearCheckpoint (param) {
+  return http.post('/vanna/v0/checkpoint/direct/cleanup', param)
+}

+ 49 - 2
src/api/dataGovernance.js

@@ -117,13 +117,18 @@ const dataModel = {
   deleteModel: (param) => {
     return http.post('/model/data/model/delete', param)
   },
+  // 通过资源id查找元数据
+  getMetaDataByModelId: (param) => {
+    return http.post('/model/search', param)
+    // return http.post('/id/data/search', param)
+  },
   // 新增模型:模型选择模型 未知
   addModelByModel: (param) => {
     return http.post('/model/data/model/add', param)
   },
   // 查看模型图谱
   getModelGraph: (param) => {
-    return http.post('/model/graph/all', param)
+    return http.post('/model/data/model/graph/all', param)
   },
   // 更新模型
   updateModel: (param) => {
@@ -135,7 +140,7 @@ const dataModel = {
   },
   // 通过ddl保存模型
   savePythonCode: (param) => {
-    return http.post('/model/python', param)
+    return http.post('/model/data/model/python', param)
   }
 }
 
@@ -223,6 +228,45 @@ const dataIndicator = {
   }
 }
 
+const dataFlow = {
+  // 获取数据流列表
+  getDataFlowList: (param) => {
+    return http.get('/dataflow/get-dataflows-list', param)
+  },
+  // 获取数据流详情
+  getDataFlowDetails: (id) => {
+    return http.get(`/dataflow/get-dataflow/${id}`)
+  },
+  // 新增数据流
+  addDataFlow: (param) => {
+    return http.post('/dataflow/add-dataflow', param)
+  },
+  // 更新数据流
+  updateDataFlow: (id, param) => {
+    return http.put(`/dataflow/update-dataflow/${id}`, param)
+  },
+  // 删除数据流
+  deleteDataFlow: (id) => {
+    return http.del(`/dataflow/delete-dataflow/${id}`)
+  },
+  // 运行数据流
+  runDataFlow: (id, param) => {
+    return http.post(`/dataflow/execute-dataflow/${id}`, param)
+  },
+  // 数据流运行进度
+  getDataFlowExecute: (id) => {
+    return http.get(`/dataflow/get-dataflow-status/${id}`)
+  },
+  // 数据流运行日志
+  getDataFlowLog: (id) => {
+    return http.get(`/dataflow/get-dataflow-logs/${id}`)
+  },
+  // AI生成脚本
+  getDataFlowScript: (param) => {
+    return http.post('/dataflow/create-script', param)
+  }
+}
+
 const LLM = {
 
   // 模型训练
@@ -301,6 +345,9 @@ export const api = {
   // 数据指标
   ...dataIndicator,
 
+  // 数据流程
+  ...dataFlow,
+
   // 大语言模型对话接口
   ...LLM,
 

+ 5 - 0
src/api/index.js

@@ -29,3 +29,8 @@ export function coreGraphYear (params) {
 export function exportTable (params) {
   return http.download('/digitizationData/core/graph/data/download', params)
 }
+
+// 翻译
+export function getTranslate (param) {
+  return http.post('/system/translate', param)
+}

+ 91 - 0
src/charts/initChart.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="white" ref="content">
+    <div ref="chart"></div>
+  </div>
+</template>
+
+<script>
+import { isIE } from '@/utils'
+
+import elementResizeDetector from 'element-resize-detector'
+export default {
+  name: 'initChart',
+  data () {
+    return {
+      chart: null,
+      resizeObserver: null
+    }
+  },
+  beforeDestroy () {
+    this.cleanup()
+  },
+  mounted () {
+    this.setupResizeObserver()
+  },
+  methods: {
+    setupResizeObserver () {
+      if (!this.$refs.content) return
+
+      if (isIE()) {
+        this.resizeDetector = elementResizeDetector()
+        this.resizeDetector.listenTo(this.$refs.content, () => {
+          if (this.chart) {
+            this.onResize()
+          }
+        })
+        return
+      }
+      this.resizeObserver = new ResizeObserver(entries => {
+        window.requestAnimationFrame(() => {
+          if (!Array.isArray(entries) || !entries.length) return
+          // 处理尺寸变化的代码
+          if (this.chart) {
+            this.onResize()
+          }
+        })
+      })
+      this.resizeObserver.observe(this.$refs.content)
+    },
+    cleanup () {
+      window.removeEventListener('resize', this.onResize)
+      if (isIE() && this.resizeDetector) {
+        this.resizeDetector.removeListener(
+          this.$refs.content,
+          this.handleResize
+        )
+      }
+      if (this.resizeObserver) {
+        this.resizeObserver.disconnect() // 停止监听
+        this.resizeObserver = null
+      }
+      if (this.chart) {
+        this.chart.dispose()
+        this.chart = null
+      }
+    },
+    init () {
+      this.chart = this.$echarts.init(this.$refs.chart, null, { renderer: 'svg' })
+      this.setRect()
+      window.addEventListener('resize', this.onResize)
+      return this.chart
+    },
+    setRect () {
+      const { height, width } = this.$refs.content.getBoundingClientRect()
+      console.log(height, width)
+      this.$refs.chart.style.width = this.$refs.content.offsetWidth + 'px'
+      this.$refs.chart.style.height = this.$refs.content.offsetHeight + 'px'
+    },
+    onResize () {
+      this.$nextTick(() => {
+        if (!this.$refs.chart || !this.chart) return
+        this.setRect()
+        this.chart.resize()
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 6 - 2
src/charts/lowCode.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <empty v-if="showEmpty" :height="height"></empty>
+    <empty v-if="showEmpty" :height="height" style="max-height: 300px;"></empty>
     <div v-show="!showEmpty">
       <div v-show="!button" :id="domId" :style="{'width':'100%','height': height + 'px','padding-top':'15px','margin':'auto'}"></div>
       <v-simple-table v-if="button" dense fixed-header :height="height-100">
@@ -72,7 +72,11 @@ export default {
   },
   computed: {
     showEmpty () {
-      const series = this.option?.series
+      if (!this.chart || !this.chart.getOption) {
+        return true
+      }
+      const option = this.chart.getOption()
+      const series = option?.series
       return this.checkEmpty(series)
     }
   },

+ 5 - 5
src/components/Dialog/index.vue

@@ -5,13 +5,13 @@
     v-model="show"
     persistent
     v-bind="$attrs"
-    :fullscreen="showDrawer"
+    :fullscreen="fullscreen"
     :max-width="dialogWidth"
     :width="dialogWidth"
   >
     <div class="d-flex" style="width: 100%;height: 100%" ref="print">
-      <v-card class="d-flex flex-column" style="width: 100%;" :style="!showDrawer ? 'max-height: 90vh' : ''">
-        <v-card-title :class="{'drawer': showDrawer}" class="justify-space-between">
+      <v-card class="d-flex flex-column" style="width: 100%;" :style="!fullscreen ? 'max-height: 90vh' : ''">
+        <v-card-title :class="{'drawer': fullscreen}" class="justify-space-between">
           <span class="text-h5">
             {{ title }}
           </span>
@@ -22,7 +22,7 @@
               </v-icon>
             </v-btn>
             <v-btn icon @click="handleClose">
-              <v-icon :color="showDrawer ? '#fff' : '#000'">
+              <v-icon :color="fullscreen ? '#fff' : '#000'">
                 mdi-close
               </v-icon>
             </v-btn>
@@ -101,7 +101,7 @@ export default {
       type: Boolean,
       default: true
     },
-    showDrawer: {
+    fullscreen: {
       type: Boolean,
       default: false
     },

+ 98 - 0
src/components/FullscreenToggle/index.vue

@@ -0,0 +1,98 @@
+<template>
+  <div ref="fullscreenContainer">
+    <slot :toggle="toggleFullscreen" :isFullscreen="isFullscreen"></slot> <!-- 这里放你的图表或其他内容 -->
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'FullscreenToggle',
+  data () {
+    return {
+      isFullscreen: false
+    }
+  },
+  mounted () {
+    this.initFullscreenListeners()
+  },
+  beforeDestroy () {
+    this.removeFullscreenListeners()
+  },
+  methods: {
+    // 检查是否全屏
+    checkFullscreen () {
+      return (
+        document.fullscreenElement ||
+        document.webkitFullscreenElement ||
+        document.msFullscreenElement
+      )
+    },
+
+    // 进入全屏
+    enterFullscreen () {
+      const container = this.$refs.fullscreenContainer
+      if (!container) return
+
+      if (container.requestFullscreen) {
+        container.requestFullscreen().catch((err) => console.error(err))
+      } else if (container.webkitRequestFullscreen) {
+        container.webkitRequestFullscreen()
+      } else if (container.msRequestFullscreen) {
+        container.msRequestFullscreen()
+      }
+    },
+
+    // 退出全屏
+    exitFullscreen () {
+      if (document.exitFullscreen) {
+        document.exitFullscreen().catch((err) => console.error(err))
+      } else if (document.webkitExitFullscreen) {
+        document.webkitExitFullscreen()
+      } else if (document.msExitFullscreen) {
+        document.msExitFullscreen()
+      }
+    },
+
+    // 切换全屏
+    toggleFullscreen () {
+      if (this.checkFullscreen()) {
+        this.exitFullscreen()
+      } else {
+        this.enterFullscreen()
+      }
+    },
+
+    // 监听全屏变化
+    handleFullscreenChange () {
+      this.isFullscreen = this.checkFullscreen()
+    },
+
+    // 监听 ESC 键
+    handleKeyDown (e) {
+      if (e.key === 'Escape' && this.isFullscreen) {
+        this.exitFullscreen()
+      }
+    },
+
+    // 初始化监听
+    initFullscreenListeners () {
+      document.addEventListener('fullscreenchange', this.handleFullscreenChange)
+      document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange)
+      document.addEventListener('msfullscreenchange', this.handleFullscreenChange)
+      document.addEventListener('keydown', this.handleKeyDown)
+    },
+
+    // 移除监听
+    removeFullscreenListeners () {
+      document.removeEventListener('fullscreenchange', this.handleFullscreenChange)
+      document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange)
+      document.removeEventListener('msfullscreenchange', this.handleFullscreenChange)
+      document.removeEventListener('keydown', this.handleKeyDown)
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 3 - 3
src/components/List/table.vue

@@ -422,10 +422,10 @@ export default {
     },
     handleSelected (data) {
       if (this.selectItem) return this.handleSelectedItem(data)
-      if (Array.isArray(data.items)) this.ids = data.value ? data.items.map(item => item?.id) : []
+      if (Array.isArray(data.items)) this.ids = data.value ? data.items.map(item => item?.[this.itemKey]) : []
       else {
-        if (data.value) this.ids.push(data.item.id)
-        else this.ids.splice(this.ids.findIndex(key => key.id === data.item.id))
+        if (data.value) this.ids.push(data.item[[this.itemKey]])
+        else this.ids.splice(this.ids.findIndex(key => key.id === data.item[this.itemKey]))
       }
       this.$emit('selected', this.ids)
     },

+ 0 - 116
src/components/MForm/index.vue

@@ -26,35 +26,6 @@
                       @change="handleChange(item)"
                       @input="item.onInput && item.onInput(item)"
                   />
-                  <!-- <v-text-field
-                      v-if="['text', 'password', 'number'].includes(item.type)"
-                      v-model="query[item.key]"
-                      :hide-details="item.hideDetails ?? false"
-                      :dense="item.dense ?? true"
-                      :style="{width: item.width}"
-                      :placeholder="item.placeholder || item.label"
-                      :outlined="item.outlined ?? true"
-                      :type="item.type"
-                      :rules="item.rules"
-                      :disabled="item.disabled"
-                      :color="item.color"
-                      :label="item.label"
-                      :autofocus="item.autofocus"
-                      :required="item.required"
-                      :class="item.class"
-                      :suffix="item.suffix"
-                      :append-icon="item.appendIcon"
-                      :clearable="item.clearable"
-                      :readonly="item.readonly"
-                      :prepend-inner-icon="item.prependInnerIcon"
-                      hide-spin-buttons
-                      @wheel.native="$event => handleWheel($event, item)"
-                      @keyup.enter.native="item.keyupEnterNative && item.keyupEnterNative(index)"
-                      @click="item.click && item.click(index)"
-                      @click:append="item.clickAppend && item.clickAppend(index)"
-                      @change="handleChange(item)"
-                      @input="item.onInput && item.onInput(item)"
-                  /> -->
                   <v-autocomplete
                       v-if="item.type === 'autocomplete'"
                       v-model="query[item.key]"
@@ -77,93 +48,6 @@
                       <slot :name="item.prependItem" :item="item"></slot>
                     </template>
                   </v-autocomplete>
-                  <!-- <v-autocomplete
-                      v-if="item.type === 'autocomplete'"
-                      v-model="query[item.key]"
-                      :attach="!item.noAttach"
-                      :placeholder="item.placeholder || item.label"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :search-input.sync="item.searchInput"
-                      :hide-details="item.hideDetails ?? false"
-                      :no-data-text="item.noDataText || 'No data available'"
-                      :rules="item.rules"
-                      :loading="item.loading"
-                      :label="item.label"
-                      :items="item.items"
-                      :disabled="item.disabled"
-                      :multiple="item.multiple"
-                      :clearable="item.clearable"
-                      :readonly="item.readonly"
-                      :hide-no-data="item.hideNoData"
-                      :hide-selected="item.hideSelected"
-                      :return-object="item.returnObject"
-                      @change="handleChange(item)"
-                  >
-                    <template v-if="item.slotAppendItem" v-slot:append-item>
-                      <slot :name="item.slotAppendItem" :item="item"></slot>
-                    </template>
-                    <template v-if="item.prependItem" #prepend-item>
-                      <slot :name="item.prependItem" :item="item"></slot>
-                    </template>
-                  </v-autocomplete> -->
-                  <!-- autocomplete2 多选纸片样式 -->
-                  <v-autocomplete
-                      v-if="item.type === 'autocomplete2'"
-                      v-model="query[item.key]"
-                      :rules="item.rules"
-                      :attach="!item.noAttach"
-                      :loading="item.loading"
-                      :label="item.label"
-                      :placeholder="item.placeholder || item.label"
-                      :items="item.canCreate ? [inputUpdateValue, ...item.items].filter(Boolean) : item.items"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :multiple="item.multiple"
-                      :clearable="item.clearable"
-                      :search-input.sync="item.searchInput"
-                      :hide-no-data="item.hideNoData"
-                      :hide-selected="item.hideSelected"
-                      :readonly="item.readonly"
-                      @change="handleChange(item)"
-                      @update:search-input="$event => item.canCreate ? inputUpdateValue = $event : inputUpdateAutocomplete($event)"
-                      :hide-details="item.hideDetails ?? false"
-                      deletable-chips
-                      cache-items
-                      small-chips
-                    ></v-autocomplete>
-                  <v-combobox
-                      v-if="item.type === 'combobox'"
-                      :rules="item.rules"
-                      v-model="query[item.key]"
-                      :attach="true"
-                      :label="item.label"
-                      :placeholder="item.placeholder || item.label"
-                      :items="item.items"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :clearable="item.clearable"
-                      :disabled="item.disabled"
-                      @change="handleChange(item)"
-                  >
-                    <template v-if="item.hasIcon" v-slot:selection="data">
-                      <v-icon color="blue darken-2">{{ data.item.label }}</v-icon>
-                    </template>
-                    <template v-if="item.hasIcon" v-slot:item="data">
-                      <v-list-item-avatar>
-                        <v-icon>{{ data.item.label }}</v-icon>
-                      </v-list-item-avatar>
-                      <v-list-item-content>
-                        {{ data.item.label }}
-                      </v-list-item-content>
-                    </template>
-                  </v-combobox>
                   <v-textarea
                     v-if="item.type === 'textarea'"
                     :rules="item.rules"

+ 181 - 0
src/components/MSearch/index.vue

@@ -0,0 +1,181 @@
+<template>
+    <v-card style="width:100%; min-height: 80px;" elevation="5">
+        <v-form v-model="valid" ref="form" @submit.prevent="search">
+          <div class="flex-horizon d-flex pa-2 flex-wrap">
+            <div v-for="(val, index) in items" :key="`filter_${val.label}_${index}`" class="pa-3" v-show="!val.hidden">
+              <v-text-field
+                  v-if="val.type === 'text'"
+                  v-model="query[val.key]"
+                  :placeholder="val.placeholder || `请输入${val.label}`"
+                  :style="`width: ${val.width || 250}px`"
+                  v-bind="val"
+                  outlined dense clearable hide-details @keyup.enter.native="search"
+              ></v-text-field>
+              <v-autocomplete
+                    v-if="val.type === 'autocomplete'"
+                    v-model="query[val.key]"
+                    :placeholder="val.placeholder || `请选择${val.label}`"
+                    :item-text="val.itemText || 'label'"
+                    :item-value="val.itemValue || 'value'"
+                    :style="`width: ${val.width || 250}px`"
+                    :search-input.sync="val.searchInput"
+                    outlined
+                    dense
+                    attach
+                    hide-details
+                    v-bind="val"
+                    @change="$event => val.change && val.change($event)"
+              ></v-autocomplete>
+              <v-select
+                v-if="val.type === 'select'"
+                v-model="query[val.key]"
+                :placeholder="val.placeholder || `请选择${val.label}`"
+                :item-text="val.itemText || 'label'"
+                :item-value="val.itemValue || 'value'"
+                :style="`width: ${val.width || 250}px`"
+                v-bind="val"
+                hide-details outlined dense attach
+                @change="$event => val.change && val.change($event)"
+              ></v-select>
+              <date-picker
+                  v-if="val.type === 'datePicker'"
+                  v-model="val.value"
+                  ref="picker"
+                  :option="{
+                    max: val.max,
+                    min: val.min,
+                    range: val.range,
+                    placeholder: val.placeholder || val.label,
+                    type: val.dateType || 'date',
+                    clearable: val.clearable || false
+                  }"
+                  @change="val.change && val.change($event), val.value = $event"
+              />
+              <m-data-picker
+                v-if="val.type === 'dataPicker'"
+                v-model="val.value"
+                :style="`width: ${val.width || 250}px`"
+                :label="val.label"
+                :placeholder="val.placeholder || val.label"
+                :item-children="val.itemChildren ?? 'children'"
+                :item-label="val.itemLabel ?? 'label'"
+                :item-value="val.itemValue ?? 'value'"
+                :items="val.items"
+                @change="$event => val.change && val.change($event)"
+              ></m-data-picker>
+              <template v-if="val.type === 'checkbox'">
+                <div class="d-flex">
+                  <v-checkbox
+                    v-model="val.value"
+                    v-for="k in val.items"
+                    :key="k.key"
+                    :label="k.label"
+                    :color="val.color"
+                    :value="k.value"
+                    hide-details
+                    :multiple="true"
+                    class="mr-3"
+                    style="margin-top: 0;padding-top: 6px;"
+                  ></v-checkbox>
+                </div>
+              </template>
+            </div>
+            <template>
+              <div class="pt-3 mr-3">
+                <v-btn color="primary" class="elevation-5 half-button" rounded @click="search">查 询</v-btn>
+              </div>
+              <div class="pt-3">
+                <v-btn color="#5cbbf6" rounded class="white--text elevation-5 half-button" @click="reset">重 置</v-btn>
+              </div>
+            </template>
+            <slot name="btn"></slot>
+          </div>
+          <slot></slot>
+        </v-form>
+        <slot name="footer"></slot>
+    </v-card>
+</template>
+
+<script>
+import DatePicker from '@/components/Form/datePicker.vue'
+import MDataPicker from '@/components/MDataPicker'
+export default {
+  name: 'MSearch',
+  components: { DatePicker, MDataPicker },
+  props: {
+    value: {
+      type: Object,
+      default: () => ({})
+    },
+    items: {
+      type: Array,
+      default: () => [],
+      required: true
+    }
+  },
+  data () {
+    return {
+      query: {
+        ...this.value
+      },
+      inputUpdateValue: '',
+      valid: false
+      // filter: {},
+      // resetData: {}
+    }
+  },
+  watch: {
+    value: {
+      handler (val) {
+        this.query = val
+      },
+      deep: true
+    },
+    query: {
+      handler (val) {
+        this.$emit('input', val)
+      },
+      deep: true
+    }
+  },
+  created () {
+    // this.option?.list.forEach(item => {
+    //   this.filter[item.key] = item.value
+    //   this.resetData[item.key] = item.value
+    // })
+  },
+  methods: {
+    search () {
+      const obj = this.option?.list.reduce((res, item) => {
+        res[item.key] = item.value
+        return res
+      }, {})
+      this.$emit('search', obj)
+    },
+    reset () {
+      if (this.$listeners.resetNoParams) {
+        this.$emit('resetNoParams')
+        return
+      }
+      this.$refs.form.reset()
+      if (this.$refs?.picker && this.$refs.picker.length) {
+        this.$refs.picker.forEach(e => {
+          e.reset()
+        })
+      }
+      const obj = this.option?.list.reduce((res, item) => {
+        res[item.key] = null
+        return res
+      }, {})
+      if (this.$listeners.customReset) {
+        this.$emit('customReset')
+        return
+      }
+      this.$emit('search', obj, true)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 215 - 54
src/router/routes.js

@@ -1,42 +1,25 @@
 export default {
   routes: [
     {
-      meun: '',
-      code: '',
       hidden: 0,
       rootId: 0,
       icon: 'mdi-home',
-      remark: '',
       type: 0,
       title: '工作台',
-      local: '',
       path: '/home',
-      urls: '',
       children: [
         {
-          meun: '',
-          code: '',
           hidden: 1,
-          rootId: 653,
           icon: '',
-          remark: '',
           type: 1,
           title: '工作台',
-          local: '',
           path: '/home',
-          urls: '',
           children: [],
           enName: 'workbench',
-          id: 654,
           redirect: '',
-          level: 2,
-          openPath: '',
           active: '',
           label: '工作台',
           sort: 0,
-          parentId: 653,
-          effectiveStatus: true,
-          parentName: 'home',
           component: 'home',
           meta: {
             keepAlive: false,
@@ -51,35 +34,19 @@ export default {
             effectiveStatus: true
           },
           name: 'index',
-          style: '',
-          alwaysShow: 0,
-          metastr: '{"keepAlive":false,"allowClick":false,"enName":"workbench","editModules":true,"title":"工作台","fullScreen":false,"target":false}',
-          open: null
+          alwaysShow: 0
         },
         {
-          meun: '',
-          code: '',
           hidden: 1,
-          rootId: 653,
-          icon: '',
-          remark: '',
           type: 1,
           title: '主页编辑',
-          local: '',
           path: '/home/edit',
-          urls: '',
           children: [],
           enName: 'Edit Home Page',
-          id: 655,
           redirect: '',
-          level: 2,
-          openPath: '',
           active: '',
           label: '主页编辑',
           sort: 0,
-          parentId: 653,
-          effectiveStatus: true,
-          parentName: '',
           component: 'home/edit',
           meta: {
             keepAlive: false,
@@ -94,23 +61,14 @@ export default {
             effectiveStatus: true
           },
           name: 'homeEdit',
-          style: '',
-          alwaysShow: 0,
-          metastr: '{"keepAlive":false,"allowClick":false,"enName":"Edit Home Page","editModules":true,"title":"主页编辑","fullScreen":true,"target":false}',
-          open: null
+          alwaysShow: 0
         }
       ],
       enName: 'workbench',
-      id: 653,
       redirect: '',
-      level: 1,
-      openPath: '',
       active: '',
       label: '工作台',
       sort: 0,
-      parentId: 0,
-      effectiveStatus: true,
-      parentName: 'efficiency',
       component: 'Layout',
       meta: {
         allowClick: false,
@@ -122,10 +80,7 @@ export default {
         effectiveStatus: true
       },
       name: 'home',
-      style: '',
-      alwaysShow: 0,
-      metastr: '{"allowClick":false,"enName":"workbench","title":"工作台","target":false}',
-      open: null
+      alwaysShow: 0
     },
     {
       meun: '',
@@ -211,6 +166,54 @@ export default {
       metastr: '{"allowClick":false,"enName":"product Knowledge Base","title":"产品知识库","target":false}',
       open: null
     },
+    {
+      hidden: 0,
+      rootId: 0,
+      icon: 'mdi-chart-box',
+      type: 0,
+      path: '/data-chart',
+      children: [
+        {
+          hidden: 1,
+          icon: '',
+          type: 1,
+          title: '数据图表',
+          path: '/data-chart',
+          children: [],
+          enName: 'data Chart',
+          redirect: '',
+          active: '',
+          label: '数据图表',
+          sort: 0,
+          component: 'dataChart',
+          meta: {
+            roles: [],
+            enName: 'data Chart',
+            icon: '',
+            title: '数据图表',
+            fullScreen: false
+          },
+          name: 'index',
+          alwaysShow: 0
+        }
+      ],
+      enName: 'data Chart',
+      redirect: '',
+      active: '',
+      sort: 0,
+      component: 'Layout',
+      meta: {
+        allowClick: false,
+        roles: [],
+        enName: 'workbench',
+        icon: 'mdi-home',
+        title: '数据图表',
+        target: false,
+        effectiveStatus: true
+      },
+      name: 'home',
+      alwaysShow: 0
+    },
     {
       meun: '',
       code: '',
@@ -2374,12 +2377,170 @@ export default {
       alwaysShow: 0,
       metastr: '{"allowClick":false,"enName":"data book","title":"数据目录","target":false}',
       open: null
+    },
+    {
+      hidden: 0,
+      rootId: 0,
+      icon: 'mdi-cog-outline',
+      type: 0,
+      path: '/model-system',
+      children: [
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '质量管理',
+          path: '/model-system/model-statistics',
+          children: [],
+          enName: 'model-statistics',
+          redirect: '',
+          active: '',
+          label: '质量管理',
+          sort: 0,
+          component: 'modelSystem/modelStatistics',
+          meta: {
+            roles: [],
+            enName: 'model-statistics',
+            icon: '',
+            title: '质量管理',
+            fullScreen: false
+          },
+          name: 'model-statistics',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: 'QA反馈管理',
+          path: '/model-system/model-qa',
+          children: [],
+          enName: 'model-qa',
+          redirect: '',
+          active: '',
+          label: 'QA反馈管理',
+          sort: 0,
+          component: 'modelSystem/modelQa',
+          meta: {
+            roles: [],
+            enName: 'model-qa',
+            icon: '',
+            title: 'QA反馈管理',
+            fullScreen: false
+          },
+          name: 'model-qa',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '典型业务知识库',
+          path: '/model-system/model-train-manage',
+          children: [],
+          enName: 'model-train-manage',
+          redirect: '',
+          active: '',
+          label: '典型业务知识库',
+          sort: 10,
+          component: 'modelSystem/modelTrainManage',
+          meta: {
+            roles: [],
+            enName: 'model-train-manage',
+            icon: '',
+            title: '典型业务知识库',
+            fullScreen: false
+          },
+          name: 'model-train',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '知识库冷启动',
+          path: '/model-system/model-train',
+          children: [],
+          enName: 'model-train',
+          redirect: '',
+          active: '',
+          label: '知识库冷启动',
+          sort: 10,
+          component: 'modelSystem/modelTrain',
+          meta: {
+            roles: [],
+            enName: 'model-train',
+            icon: '',
+            title: '知识库冷启动',
+            fullScreen: false
+          },
+          name: 'model-train',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '内存管理',
+          path: '/model-system/model-history',
+          children: [],
+          enName: 'model-history',
+          redirect: '',
+          active: '',
+          label: '内存管理',
+          sort: 0,
+          component: 'modelSystem/modelHistory',
+          meta: {
+            roles: [],
+            enName: 'model-history',
+            icon: '',
+            title: '内存管理',
+            fullScreen: false
+          },
+          name: 'model-history',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '备份与恢复',
+          path: '/model-system/backup',
+          children: [],
+          enName: 'backup',
+          redirect: '',
+          active: '',
+          label: '备份与恢复',
+          sort: 10,
+          component: 'modelSystem/backup',
+          meta: {
+            roles: [],
+            enName: 'backup',
+            icon: '',
+            title: '备份与恢复',
+            fullScreen: false
+          },
+          name: 'backup',
+          alwaysShow: 0
+        }
+      ],
+      name: 'model-system',
+      enName: 'modelSystem',
+      redirect: '',
+      active: '',
+      sort: 100,
+      component: 'Layout',
+      meta: {
+        allowClick: false,
+        roles: [],
+        enName: 'modelSystem',
+        icon: 'mdi-cog-outline',
+        title: '知识库管理',
+        target: false,
+        effectiveStatus: true
+      },
+      alwaysShow: 0
     }
   ],
-  permission: [
-    'systemManage:roleManage:create',
-    'systemManage:roleManage:edit',
-    'systemManage:roleManage:delete',
-    'systemManage:roleManage:data'
-  ]
+  permission: []
 }

+ 4 - 2
src/store/modules/user.js

@@ -6,9 +6,11 @@ import { setToken, deleteToken } from '@/utils/auth'
 
 // const defaultSingleSignOn = localStorage.getItem('SET_SINGLE_SIGN_ON') !== '0' // 默认单点登录
 
+const userInfo = localStorage.getItem('userInfo')
+
 const state = {
   singleSignOn: localStorage.getItem('SET_SINGLE_SIGN_ON') !== '0', // 单点登录
-  userInfo: {}
+  userInfo: userInfo && userInfo !== 'undefined' ? JSON.parse(userInfo) : {}
 }
 
 const mutations = {
@@ -39,7 +41,7 @@ const actions = {
         commit('SET_SINGLE_SIGN_ON', 0)
         setToken(data.token)
         // const { username, name, id, employeeCode } = data.user
-        commit('SET_USERINFO', data.user)
+        commit('SET_USERINFO', data)
         resolve()
       }).catch(err => { reject(err) })
     })

+ 4 - 0
src/utils/index.js

@@ -31,6 +31,10 @@ export function generateUUID () {
   return uuid.replace(/-/g, '')
 }
 
+export function isIE () {
+  return !!document.documentMode
+}
+
 /**
  *  驼峰转下划线
  * @param {String} str

+ 5 - 4
src/utils/request.js

@@ -54,7 +54,7 @@ service.interceptors.response.use(
       return Promise.reject(res)
     }
     if (res.code !== 200) {
-      if (res.data && Object.keys(res.data).length) {
+      if (res.data && Object.keys(res.data)?.length) {
         return Promise.reject(res)
       }
       return Promise.reject(res.message)
@@ -62,20 +62,21 @@ service.interceptors.response.use(
     return res
   },
   error => {
-    return Promise.reject(error)
+    return Promise.reject(error.message || error)
   }
 )
 
 // 请求方法
 const http = {
-  post (url, params) {
+  post (url, params, config = {}) {
     return service.post(url, params, {
       transformRequest: [(params) => {
         return JSON.stringify(params)
       }],
       headers: {
         'Content-Type': 'application/json'
-      }
+      },
+      ...config
     })
   },
   get (url, params) {

+ 29 - 25
src/views/dataBook/components/mGraph.vue

@@ -84,21 +84,21 @@ const NODES_SIZE = {
   width: 30,
   height: 30
 }
-const LINE_COLOR_MAP = {
-  包含: '#F44336', // red
-  影响: '#E91E63', // pink
-  依赖: '#3F51B5', // indigo
-  来源: '#4CAF50', // green
-  引用: '#9C27B0', // purple
-  继承: '#673AB7', // deep-purple
-  标记: '#2196F3', // blue
-  使用: '#03A9F4', // light-blue
-  关联: '#00BCD4', // cyan
-  拥有: '#009688', // teal
-  下级: '#8BC34A', // light-green
-  上级: '#CDDC39', // lime
-  联动: '#FF9800' // orange
-}
+// const LINE_COLOR_MAP = {
+//   包含: '#F44336', // red
+//   影响: '#E91E63', // pink
+//   依赖: '#3F51B5', // indigo
+//   来源: '#4CAF50', // green
+//   引用: '#9C27B0', // purple
+//   继承: '#673AB7', // deep-purple
+//   标记: '#2196F3', // blue
+//   使用: '#03A9F4', // light-blue
+//   关联: '#00BCD4', // cyan
+//   拥有: '#009688', // teal
+//   下级: '#8BC34A', // light-green
+//   上级: '#CDDC39', // lime
+//   联动: '#FF9800' // orange
+// }
 export default {
   name: 'details-graph',
   components: { RelationGraph, MEmpty },
@@ -207,19 +207,19 @@ export default {
         ]
       },
       config: {
-        resource: { // 资源
+        DataResource: { // 资源
           color: '#9FA8DA',
           title: '数据资源',
           className: 'sourceNode',
           ...NODES_SIZE
         },
-        model: { // 模型
+        DataModel: { // 模型
           color: '#EF9A9A',
           title: '数据模型',
           className: 'modelNode',
           ...NODES_SIZE
         },
-        metric: { // 指标
+        DataMetric: { // 指标
           color: '#00BCD4',
           title: '数据指标',
           className: 'metricNode',
@@ -231,13 +231,13 @@ export default {
         //   className: 'standardNode',
         //   ...NODES_SIZE
         // },
-        label: { // 标签
+        DataLabel: { // 标签
           color: '#9C27B0',
           title: '数据标签',
           className: 'labelNode',
           ...NODES_SIZE
         },
-        meta: {
+        DataMeta: { // 元数据
           color: defaultNodeColor,
           title: '元数据',
           className: '',
@@ -307,17 +307,21 @@ export default {
         this.graphOptions.downloadImageFileName = data.rootId ?? ''
 
         data.nodes.forEach(ele => {
-          if (!this.config[ele.type]) {
+          if (!this.config[ele.node_type]) {
             return
           }
-          Object.assign(ele, this.config[ele.type])
+          ele.text = ele.name
+          Object.assign(ele, this.config[ele.node_type])
         })
 
-        data.lines.forEach(ele => {
-          ele.color = LINE_COLOR_MAP[ele.text]
-        })
+        // data.lines.forEach(ele => {
+        //   ele.color = LINE_COLOR_MAP[ele.text]
+        // })
         this.$nextTick(() => {
           this.$refs.graphRef.setOptions(this.graphOptions, async (graphInstance) => {
+            if (!this.$refs.graphRef || !this.$refs.graphRef.setJsonData) {
+              return
+            }
             this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
               await _graphInstance.setZoom(75)
               successCallback()

+ 7 - 1
src/views/dataBook/components/mGraphList.vue

@@ -91,7 +91,7 @@ export default {
     })
   },
   methods: {
-    async init (query = { tag: null }) {
+    async init (query = { tag: null, type: 'community' }) {
       this.loading = true
       try {
         const { data } = await this.toApi(query)
@@ -102,6 +102,12 @@ export default {
         }
         this.isEmpty = false
         this.$nextTick(() => {
+          if (!this.$refs.graphRef || !this.$refs.graphRef.setJsonData) {
+            return
+          }
+          data.nodes.forEach((item) => {
+            item.text = item.text ?? item.name
+          })
           this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
             this.loading = false
           })

+ 1 - 1
src/views/dataBook/dataMetaData/components/metaDataInfo.vue

@@ -14,7 +14,7 @@
         <ul>
           <li>{{ info.name }}</li>
           <li>{{ info.en_name }}</li>
-          <li>{{ info.type }}</li>
+          <li>{{ info.data_type }}</li>
           <li>{{ info.time }}</li>
           <li></li>
           <!-- <li>{{ info.system == 0 ? '系统级' : '用户级' }}</li> -->

+ 1 - 1
src/views/dataBook/dataModel/details.vue

@@ -41,7 +41,7 @@ export default {
         },
         {
           title: '元数据成员',
-          path: 'metaData'
+          path: 'metadata'
         },
         {
           title: '数据生产线',

+ 0 - 17
src/views/dataBook/dataModel/dynamic/metaData.vue

@@ -1,17 +0,0 @@
-<template>
-  <Metadata></Metadata>
-</template>
-
-<script>
-import Metadata from '../../dataResource/dynamic/metadata'
-export default {
-  name: 'data-book-model-meta-data',
-  components: {
-    Metadata
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-
-</style>

+ 78 - 0
src/views/dataBook/dataModel/dynamic/metadata.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="pa-3">
+    <m-card title="元数据成员">
+      <div class="pa-3">
+        <m-table
+          :headers="headers"
+          :items="items"
+          :elevation="0"
+          :is-tools="false"
+          :show-select="false"
+          :loading="loading"
+          :page-info="pageInfo"
+          :total="total"
+          @pageHandleChange="pageHandleChange"
+        ></m-table>
+      </div>
+    </m-card>
+  </div>
+</template>
+
+<script>
+import MCard from '@/components/MCard'
+import MTable from '@/components/List/table'
+import { api } from '@/api/dataGovernance'
+export default {
+  name: 'data-book-model-metadata',
+  components: {
+    MCard,
+    MTable
+  },
+  data () {
+    return {
+      loading: false,
+      headers: [
+        { text: '名称', value: 'name' },
+        { text: '英文名称', value: 'en_name' },
+        { text: '类型', value: 'data_type' },
+        { text: '创建时间', value: 'create_time' }
+      ],
+      items: [],
+      total: 0,
+      pageInfo: {
+        current: 1,
+        size: 10
+      }
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      const id = this.$route.params.id
+      if (!id) {
+        return
+      }
+      this.loading = true
+      try {
+        const { data } = await api.getMetaDataByModelId({ id: +id, ...this.pageInfo })
+        this.items = data.records
+        this.total = data.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    pageHandleChange (index) {
+      this.pageInfo.current = index
+      this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 15 - 0
src/views/dataBook/dataProcess/details.vue

@@ -0,0 +1,15 @@
+<template>
+  <div>
+
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'dataProcessDetails'
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 94 - 3
src/views/dataBook/dataProcess/index.vue

@@ -1,12 +1,103 @@
 <template>
-  <div>
-
+  <div class="white pa-3">
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      :show-select="false"
+      :is-tools="false"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #name_zh="{ item }">
+        <span class="defaultLink">{{ item.name_zh }}</span>
+      </template>
+      <template #status="{ item }">
+        <v-chip small :color="item.status === 'active' ? 'success' : 'error'">
+          {{ item.status === 'active' ? '启用' : '禁用' }}
+        </v-chip>
+      </template>
+    </m-table>
   </div>
 </template>
 
 <script>
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import { api } from '@/api/dataGovernance'
 export default {
-  name: 'dataProcess'
+  name: 'dataBookDataProcess',
+  components: { MFilter, MTable },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      filter: {
+        list: [
+          { type: 'textField', value: '', label: '关键词', key: 'title' }
+        ]
+      },
+      queryData: {
+        title: null
+      },
+      headers: [
+        { text: '中文名称', align: 'start', value: 'name_zh' },
+        { text: '英文名称', align: 'start', value: 'name_en' },
+        { text: '状态', align: 'center', value: 'status' },
+        { text: '分类', align: 'center', value: 'category' },
+        { text: '作者', align: 'center', value: 'leader' },
+        { text: '机构', align: 'center', value: 'organization' },
+        { text: '描述', align: 'start', value: 'describe' },
+        { text: '创建时间', align: 'start', value: 'created_at' },
+        { text: '更新时间', align: 'start', value: 'updated_at' }
+      ],
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await api.getDataFlowList({
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size,
+          search: this.queryData.title || undefined
+        })
+        this.items = data.list
+        this.total = data.pagination.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    }
+  }
 }
 </script>
 

+ 2 - 2
src/views/dataBook/dataResource/dynamic/metadata.vue

@@ -34,8 +34,8 @@ export default {
       headers: [
         { text: '名称', value: 'name' },
         { text: '英文名称', value: 'en_name' },
-        { text: '类型', value: 'type' },
-        { text: '创建时间', value: 'time' }
+        { text: '类型', value: 'data_type' },
+        { text: '创建时间', value: 'create_time' }
       ],
       items: [],
       total: 0,

+ 3 - 15
src/views/dataBook/dataResource/index.vue

@@ -1,8 +1,7 @@
 <template>
-  <div class="pa-3 white" :class="{content: switchShow}">
-    <m-filter switchover @search="handleSearch" v-model="switchShow"></m-filter>
+  <div class="pa-3 white">
+    <m-filter @search="handleSearch"></m-filter>
     <m-table
-      v-if="!switchShow"
       class="mt-3"
       :loading="loading"
       :headers="headers"
@@ -45,9 +44,6 @@
         <v-chip v-for="chip in item.alias" :key="chip">{{ chip }}</v-chip>
       </template>
     </m-table>
-    <div v-else style="flex: 1" class="mt-3">
-      <m-graph-list ref="graph" download-image-file-name="数据资源图谱"></m-graph-list>
-    </div>
   </div>
 </template>
 
@@ -55,7 +51,6 @@
 import MFilter from '../../dataGovernance/components/Filter'
 import MTable from '@/components/List/table.vue'
 import MGraphDrill from '../components/mGraphDrill'
-import MGraphList from '../components/mGraphList'
 import { api } from '@/api/dataGovernance'
 import {
   frequency,
@@ -63,11 +58,10 @@ import {
 } from '@/utils/dataGovernance'
 export default {
   name: 'data-book-resource',
-  components: { MFilter, MTable, MGraphDrill, MGraphList },
+  components: { MFilter, MTable, MGraphDrill },
   data () {
     return {
       api,
-      switchShow: false,
       loading: false,
       show: false,
       queryData: {
@@ -126,12 +120,6 @@ export default {
         })
     },
     handleSearch (val) {
-      if (this.switchShow) {
-        this.$nextTick(() => {
-          this.$refs.graph.init(val)
-        })
-        return
-      }
       Object.assign(this.queryData, val)
       this.pageInfo.current = 1
       this.init()

+ 86 - 0
src/views/dataChart/dataChartConversation.vue

@@ -0,0 +1,86 @@
+<template>
+  <v-navigation-drawer
+    v-model="drawer"
+    absolute
+    temporary
+    right
+    width="300"
+    overlay-opacity="0"
+  >
+    <div v-loading="loading">
+      <v-list dense>
+        <v-list-item-group
+          v-model="selected"
+          color="primary"
+          @change="onSelectConversation"
+        >
+          <v-list-item
+            v-for="(conversation) in conversationList"
+            :key="conversation.conversation_id"
+          >
+            <v-list-item-content>
+              <v-list-item-title>{{ conversation.conversation_title }}</v-list-item-title>
+            </v-list-item-content>
+          </v-list-item>
+        </v-list-item-group>
+      </v-list>
+    </div>
+  </v-navigation-drawer>
+</template>
+
+<script>
+import {
+  getConversations,
+  getConversationsById
+} from '@/api/dataChart'
+export default {
+  name: 'dataChartConversation',
+  data () {
+    return {
+      loading: false,
+      drawer: false,
+      selected: null,
+      lastSelected: null,
+      conversationList: []
+    }
+  },
+  methods: {
+    target () {
+      this.drawer = !this.drawer
+      if (this.drawer) {
+        this.getConversationList()
+      }
+    },
+    async getConversationList () {
+      this.loading = true
+      try {
+        const { data } = await getConversations({ limit: 5 })
+        this.conversationList = data.conversations
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async onSelectConversation (index) {
+      if (!index) {
+        setTimeout(() => {
+          this.selected = this.lastSelected
+        })
+        return
+      }
+      this.lastSelected = index
+      try {
+        const { data } = await getConversationsById(this.conversationList[index].conversation_id)
+        this.$emit('update', data)
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 204 - 0
src/views/dataChart/dataChartEdit.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="d-flex chart heightFull widthFull">
+    <div v-if="showChart" class="chart-list heightFull overflow-y-auto mr-3">
+      <div v-for="(chart, key) in Charts" :key="key" class="chart-type mb-3" @click="onChange(key)">
+        <div>
+          <span class="mdi" :class="chart.icon"></span>
+        </div>
+        <div>
+          {{ chart.title }}
+        </div>
+      </div>
+    </div>
+
+    <div class="chart-content d-flex heightFull overflow-hidden">
+      <FullscreenToggle v-if="showChart" class="chart-content-show heightFull white mr-3 overflow-hidden pa-3 position-relative" ref="box">
+        <template v-slot="{ toggle, isFullscreen }">
+          <div class="position-absolute d-flex flex-column" style="right: 10px; top: 80px; z-index: 999">
+            <v-btn
+              icon
+              color="primary"
+              @click="toggle"
+            >
+              <v-icon>{{ isFullscreen ? 'mdi-arrow-collapse' : 'mdi-arrow-expand'}}</v-icon>
+            </v-btn>
+            <v-btn
+              icon
+              color="primary"
+              class="mt-3"
+              @click="onClose"
+            >
+              <v-icon>mdi-close</v-icon>
+            </v-btn>
+          </div>
+          <InitChart ref="chart" class="heightFull widthFull"></InitChart>
+        </template>
+      </FullscreenToggle>
+      <DataChartEditChat ref="dataChartEditChatRefs" :class="showChart ? 'widthHalf' : 'widthFull'" @render="onRender">
+        <v-btn
+          class="history"
+          color="indigo lighten-1"
+          dark
+          @click.stop="onClick"
+        >
+          最近会话
+        </v-btn>
+      </DataChartEditChat>
+      <DataChartConversation ref="dataChartConversationRefs" @update="onUpdate"></DataChartConversation>
+    </div>
+  </div>
+</template>
+
+<script>
+// 属性模块
+import * as Charts from './utils/options.js'
+import DataChartEditChat from './dataChartEditChat.vue'
+import InitChart from '@/charts/initChart'
+import { cloneDeep } from 'lodash'
+import DataChartConversation from './dataChartConversation.vue'
+import FullscreenToggle from '@/components/FullscreenToggle'
+export default {
+  name: 'dataChartEdit',
+  components: {
+    DataChartEditChat,
+    InitChart,
+    FullscreenToggle,
+    DataChartConversation
+  },
+  data () {
+    return {
+      showChart: false,
+      Charts,
+      chart: null,
+      chartsOpt: {
+        data: [[]],
+        config: {
+          xAxisData: []
+        },
+        key: null
+      }
+    }
+  },
+  methods: {
+    onClose () {
+      this.showChart = false
+      this.chart = null
+    },
+    onChange (key) {
+      this.chartsOpt.key = key
+      this.setData()
+    },
+    onRender ({ type, data }) {
+      this.showChart = true
+      this.$nextTick(() => {
+        if (!this.chart) {
+          this.chart = this.$refs.chart.init()
+        }
+        this.chartsOpt.config.xAxisData = type
+        this.chartsOpt.data = data
+        this.chartsOpt.key = this.chartsOpt.key || 'bar'
+        this.setData()
+      })
+    },
+    setData () {
+      const { data, key, config } = this.chartsOpt
+      this.chart.showLoading()
+      // 根据key值处理data
+      const _option = cloneDeep(Charts[key].option)
+      const series = _option.series
+      const _data = []
+      if (key === 'pie') {
+        (data || [[]]).forEach(e => {
+          _data.push(e.map((_e, i) => {
+            return {
+              value: _e,
+              name: config.xAxisData[i] ?? _e
+            }
+          }))
+        })
+      } else {
+        _data.push(...data)
+      }
+      const _tem = series[0]
+      const { data: dataSource, ...opt } = _tem
+      data.forEach((d, i) => {
+        series[i] = {
+          data: _data[i],
+          ...opt
+        }
+      })
+      if (_option.xAxis?.data) {
+        _option.xAxis.data = config?.xAxisData ?? data[0].map((e, i) => i)
+      }
+      this.chart.setOption(_option, true)
+      this.chart.hideLoading()
+    },
+    onClick () {
+      this.$refs.dataChartConversationRefs.target()
+    },
+    onUpdate (data) {
+      this.$refs.dataChartEditChatRefs.update(data)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.heightFull {
+  height: 100%;
+}
+.widthFull {
+  width: 100%;
+}
+
+.widthHalf {
+  width: 50%;
+}
+.chart {
+  &-list {
+    // width: 100px;
+    .chart-type {
+      width: 100%;
+      height: 60px;
+      box-sizing: border-box;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 10px;
+      border: 1px solid #ccc;
+      border-radius: 5px;
+      cursor: pointer;
+      .mdi {
+        font-size: 24px;
+      }
+      &:hover {
+        background-color: #f5f5f5;
+      }
+    }
+  }
+  &-content {
+    width: 0;
+    flex: 1;
+    position: relative;
+    &-show {
+      width: 50%;
+      border: 1px solid #ccc !important;
+    }
+  }
+}
+.position {
+  &-relative {
+    position: relative;
+  }
+  &-absolute {
+    position: absolute;
+  }
+}
+.history {
+  position: absolute;
+  right: 20px;
+  top: 20px;
+  z-index: 1;
+}
+</style>

+ 477 - 0
src/views/dataChart/dataChartEditChat.vue

@@ -0,0 +1,477 @@
+<template>
+  <div class="chart-content-chat heightFull d-flex flex-column position-relative overflow-hidden">
+    <slot></slot>
+    <div class="chart-content-chat-title mb-3">
+      <v-tabs>
+        <v-tab>AI 取数</v-tab>
+      </v-tabs>
+    </div>
+    <div class="chart-content-chat-box overflow-y-auto position-relative element" ref="chatBox" v-loading="loading">
+      <div class="pa-3">
+        <div
+          v-for="(item, index) in items"
+          :key="index"
+          :class="['d-flex', 'mb-3', item.type === 1 ? 'flex-row' : 'flex-row-reverse' ]"
+        >
+          <v-avatar color="indigo" size="36">
+            <span class="white--text">{{ item.type === 1 ? 'AI' : 'T' }}</span>
+          </v-avatar>
+          <div :class="[item.type === 1 ? 'ml-3 flex-grow-1 flex-shrink-1' : 'mr-3 box-length-70']">
+            <div
+              :class="['d-flex align-center', `justify-${item.type === 1 ? 'start' : 'end'}`]"
+            >
+              {{ item.type === 1 ? 'AI助手' : '游客' }}
+              <template v-if="item.type === 1">
+                <v-btn
+                  v-if="item.content.sql"
+                  class="ml-3"
+                  small
+                  elevation="0"
+                  depressed
+                  @click="item.showSnackbar = !item.showSnackbar"
+                >
+                  {{ !item.showSnackbar ? '查看SQL' : '收起SQL'}}
+                </v-btn>
+              </template>
+            </div>
+            <div class="mt-2" :class="{ 'indigo lighten-5 pa-3 rounded': item.type === 2 }">
+              <template v-if="typeof item.content === 'string'">
+                <div>
+                  <span v-if="item.welcome" class="mdi mdi-hand-wave"></span>
+                  {{item.content}}
+                </div>
+                <div v-if="item.welcome">
+                  <div class="mt-1" v-for="_item in item.items" :key="_item">
+                    <span class="defaultLink" @click="onSend(_item)">{{_item}}</span>
+                  </div>
+                </div>
+              </template>
+              <template v-else-if="Object.keys(item.content).length === 0">
+                <span>
+                  正在思考中
+                  <v-progress-circular
+                    indeterminate
+                    size="14"
+                    class="ml-1"
+                    width="2"
+                    color="primary"
+                  ></v-progress-circular>
+                </span>
+              </template>
+              <template v-else>
+                <div>
+                  {{ item.content.response }}
+                </div>
+                <div v-if="item.showSnackbar" class="pa-3 blue-grey lighten-3 mt-3">
+                  {{ item.content.sql }}
+                </div>
+                <div class="mt-3" v-if="item.content.records && item.content.records.columns.length">
+                  <div>
+                    <v-menu
+                      v-model="item.showMenu"
+                      :close-on-content-click="false"
+                      :close-on-click="false"
+                      max-width="300"
+                      attach=".chart-content-chat-box"
+                    >
+                      <template v-slot:activator="{ on , attrs}">
+                        <v-btn
+                          v-on="on"
+                          v-bind="attrs"
+                          text
+                          color="primary"
+                        >我要画图</v-btn>
+                      </template>
+                      <div class="white">
+                        <v-banner>画图配置</v-banner>
+                        <div class="pa-3">
+                          <v-autocomplete
+                            v-model="item.model.typeAxis"
+                            :items="item.content.records.columns"
+                            class="mb-3"
+                            outlined
+                            dense
+                            hide-details
+                            label="X轴"
+                          ></v-autocomplete>
+
+                          <v-autocomplete
+                            v-model="item.model.dataAxis"
+                            :items="item.content.records.columns"
+                            class="mb-3"
+                            outlined
+                            dense
+                            hide-details
+                            label="Y轴"
+                            multiple
+                            chips
+                            small-chips
+                          ></v-autocomplete>
+                          <div class="text-right">
+                            <v-btn small class="mr-3" @click="item.showMenu = false">关闭</v-btn>
+                            <v-btn small color="primary" @click="onRender(item)">图表预览</v-btn>
+                          </div>
+                        </div>
+                      </div>
+                    </v-menu>
+                  </div>
+                  <v-card flat outlined height="324">
+                    <div class="pa-3">
+                      <v-simple-table
+                        fixed-header
+                        dense
+                        height="300px"
+                      >
+                        <template v-slot:default>
+                          <thead>
+                            <tr>
+                              <th
+                                v-for="header in item.content.records.columns"
+                                :key="header"
+                                class="text-left"
+                              >{{ header }}</th>
+                            </tr>
+                          </thead>
+                          <tbody>
+                            <tr
+                              v-for="(row, index) in item.content.records.rows"
+                              :key="index"
+                            >
+                              <td
+                                v-for="header in item.content.records.columns"
+                                :key="header"
+                                class="text-left"
+                              >{{ row[header] }}</td>
+                            </tr>
+                          </tbody>
+                        </template>
+                      </v-simple-table>
+                    </div>
+                  </v-card>
+                </div>
+                <div class="d-flex align-center" v-if="!item.dataValidation">
+                  您认为结果是否正确
+                  <v-btn
+                    class="ma-2"
+                    text
+                    icon
+                    small
+                    color="blue lighten-2"
+                    @click="onAddTrain(item, true)"
+                  >
+                    <v-icon>mdi-thumb-up</v-icon>
+                  </v-btn>
+
+                  <v-btn
+                    class="ma-2"
+                    text
+                    icon
+                    small
+                    color="red lighten-2"
+                    @click="onAddTrain(item, false)"
+                  >
+                    <v-icon>mdi-thumb-down</v-icon>
+                  </v-btn>
+                </div>
+              </template>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="pa-3 chart-content-chat-btn">
+      <div class="send">
+        <div class="pa-3 text-center" v-if="conversationId">
+          <v-btn color="indigo" outlined small @click="onNew">
+            <v-icon left>
+              mdi-chat-plus-outline
+            </v-icon>新对话
+          </v-btn>
+        </div>
+        <div class="send-box">
+          <v-textarea
+            v-model="question"
+            class="send-box-area"
+            auto-grow
+            placeholder="请输入您想问的内容,按 Ctrl+Enter 换行"
+            outlined
+            hide-details
+            no-resize
+            rows="1"
+            @keydown.enter="handleKeyCode($event)"
+          >
+          </v-textarea>
+          <v-btn icon color="primary" class="btn" :disabled="!question || disabled" @click="handleSendMsg">
+            <v-icon>mdi-send</v-icon>
+          </v-btn>
+        </div>
+        <div>
+          <v-chip-group
+            active-class="primary--text"
+            column
+            v-model="routingMode"
+          >
+            <v-chip
+              v-for="chip in chips"
+              :key="chip.value"
+              small
+              :value="chip.value"
+            >
+              {{ chip.text }}
+            </v-chip>
+          </v-chip-group>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  getAsk,
+  addFeedback
+} from '@/api/dataChart'
+import { mapGetters } from 'vuex'
+export default {
+  name: 'dataChartEditChat',
+  data () {
+    return {
+      routingMode: undefined,
+      chips: [
+        { text: '聊天模式', value: 'chat_direct' },
+        { text: '数据库模式', value: 'database_direct' }
+      ],
+      loading: false,
+      disabled: false,
+      question: '',
+      items: [
+        {
+          type: 1,
+          welcome: true,
+          content: '你好,我是您的数据查询小助手,支持查询“高速公路服务区”的相关信息。您可以这样提问: ',
+          items: [
+            '现在一共有多少个服务区?分别归属于哪些管理公司?',
+            '哪个服务区的档口数量最多?',
+            '赣州分公司下,餐饮类档口的日均订单量是多少?'
+          ]
+        }
+      ],
+      abortController: null,
+      conversationId: undefined
+      // trueData: false
+    }
+  },
+  computed: {
+    ...mapGetters(['userInfo'])
+  },
+  methods: {
+    onNew () {
+      this.abortController.abort('')
+      this.abortController = null
+      this.items.splice(1, this.items.length - 1)
+      this.conversationId = undefined
+    },
+    handleKeyCode (event) {
+      if (event.keyCode === 13) {
+        if (!event.ctrlKey) {
+          event.preventDefault()
+          this.handleSendMsg()
+        } else {
+          this.question += '\n'
+        }
+      }
+    },
+    onSend (str) {
+      if (this.disabled) {
+        return
+      }
+      this.question = str
+      this.handleSendMsg()
+    },
+    async handleSendMsg () {
+      if (!this.question || this.disabled) {
+        return
+      }
+      this.disabled = true
+
+      const question = this.question
+      this.items.push({
+        type: 2,
+        user: '游客',
+        content: question
+      })
+      this.scrollToBottom()
+      const ask = {
+        type: 1,
+        content: {},
+        showSnackbar: false,
+        dataValidation: false,
+        question, // 记录当前问题
+        showMenu: false,
+        model: {
+          dataAxis: null,
+          typeAxis: null
+        }
+      }
+      this.items.push(ask)
+      this.question = ''
+      try {
+        this.abortController = new AbortController()
+        const { data } = await getAsk({
+          question,
+          user_id: this.userInfo.id,
+          routing_mode: this.routingMode,
+          conversation_id: this.conversationId
+        }, {
+          signal: this.abortController.signal
+        })
+        ask.content = data
+        this.conversationId = data.conversation_id
+        this.scrollToBottom()
+      } catch (error) {
+        ask.content = error.message ?? error
+      } finally {
+        this.disabled = false
+      }
+    },
+    scrollToBottom () {
+      this.$nextTick(() => {
+        const box = this.$refs.chatBox
+        if (!box) {
+          return
+        }
+        box.scrollTop = box.scrollHeight
+      })
+    },
+    async onAddTrain (item, bool) {
+      this.loading = true
+      try {
+        await addFeedback({
+          question: item.question,
+          sql: item.content.sql,
+          is_thumb_up: bool,
+          user_id: this.userInfo.id
+        })
+        item.dataValidation = true
+        // this.$snackbar.success('操作成功')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    onRender ({ model, content }) {
+      if (!model.dataAxis || !model.typeAxis) {
+        this.$snackbar.error('请选择数据轴和类型轴')
+        return
+      }
+      const { typeAxis, dataAxis } = model
+      const data = {
+        type: typeAxis ? content.records.rows.map(e => e[typeAxis]) : [],
+        data: dataAxis ? dataAxis.map(e => content.records.rows.map(r => r[e])) : []
+      }
+      this.$emit('render', data)
+    },
+    update (data) {
+      if (this.abortController && this.disabled) {
+        this.abortController.abort('')
+        this.abortController = null
+      }
+      this.items.splice(1, this.items.length - 1, ...data.messages.map(e => {
+        if (e.role === 'user') {
+          return {
+            type: 2,
+            user: '游客',
+            content: e.content
+          }
+        }
+        return {
+          type: 1,
+          content: e.metadata,
+          showSnackbar: false,
+          dataValidation: false,
+          question: e.content, // 记录当前问题
+          showMenu: false,
+          model: {
+            dataAxis: null,
+            typeAxis: null
+          }
+        }
+      }))
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.heightFull {
+  height: 100%;
+}
+.widthFull {
+  width: 100%;
+}
+.chart-content {
+  &-chat {
+    border: 1px solid #ccc;
+    &-box {
+      height: 0;
+      flex: 1;
+      max-width: 800px;
+      width: 100%;
+      margin: 0 auto;
+    }
+    &-btn {
+      .send {
+        margin: 0 auto;
+        max-width: 800px;
+      }
+    }
+  }
+
+}
+.box-length-70 {
+  max-width: 70%;
+}
+.send {
+  // height: 130px;
+  // margin: 20px 0;
+  padding: 20px;
+  &-box {
+    width: 100%;
+    // max-width: 800px;
+    position: relative;
+    .btn {
+      position: absolute;
+      right: 20px;
+      bottom: 12px;
+    }
+    &-area {
+      position: relative;
+      bottom: 0;
+      ::v-deep textarea {
+        padding: 15px 70px 15px 0 !important;
+        max-height: 300px;
+        min-height: 60px;
+        overflow: auto;
+        margin: 0 !important;
+      }
+    }
+  }
+}
+
+.position {
+  &-relative {
+    position: relative;
+  }
+}
+
+.element {
+  overflow: auto;
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* IE/Edge */
+}
+
+.element::-webkit-scrollbar {
+  display: none; /* Chrome/Safari/Opera */
+}
+</style>

+ 53 - 0
src/views/dataChart/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="pa-3 white">
+    <MTable
+      :headers="headers"
+      :items="items"
+      :show-select="false"
+      :loading="loading"
+      :can-delete="false"
+      @add="onAdd"
+    ></MTable>
+    <MDialog :visible.sync="show" title="图表编辑" fullscreen :footer="false">
+      <DataChartEdit v-if="show"></DataChartEdit>
+    </MDialog>
+  </div>
+</template>
+
+<script>
+import MTable from '@/components/List/table'
+import MDialog from '@/components/Dialog'
+import DataChartEdit from './dataChartEdit.vue'
+export default {
+  name: 'dataChart',
+  components: {
+    MTable,
+    MDialog,
+    DataChartEdit
+  },
+  data () {
+    return {
+      headers: [
+        { text: '标题', value: 'title' },
+        { text: '描述', value: 'describe' },
+        { text: '创建日期', value: 'createDate' },
+        { text: '操作', value: 'action' }
+      ],
+      items: [],
+      loading: false,
+      show: false
+    }
+  },
+  created () {
+  },
+  methods: {
+    onAdd () {
+      this.show = true
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 106 - 0
src/views/dataChart/utils/options.js

@@ -0,0 +1,106 @@
+// const COLOR = ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00']
+
+export const line = {
+  title: '折线图',
+  icon: 'mdi-chart-line',
+  option: {
+    legend: {
+      show: true
+    },
+    xAxis: {
+      type: 'category',
+      data: []
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        data: [],
+        type: 'line',
+        smooth: true
+      }
+    ]
+  }
+}
+
+export const bar = {
+  title: '柱状图',
+  icon: 'mdi-chart-bar',
+  option: {
+    legend: {
+      show: true,
+      left: 'center'
+    },
+    xAxis: {
+      type: 'category',
+      data: []
+    },
+    yAxis: {
+      type: 'value'
+    },
+    series: [
+      {
+        data: [],
+        type: 'bar'
+      }
+    ]
+  }
+}
+
+export const pie = {
+  title: '饼图',
+  icon: 'mdi-chart-pie',
+  option: {
+    // title: {
+    //   text: 'Referer of a Website',
+    //   subtext: 'Fake Data',
+    //   left: 'center'
+    // },
+    tooltip: {
+      trigger: 'item'
+    },
+    legend: {
+      left: 'center'
+    },
+    series: [
+      {
+        name: 'Access From',
+        type: 'pie',
+        radius: '50%',
+        data: [],
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          }
+        }
+      }
+    ]
+  }
+}
+
+// // 散点
+// export const scatter = {
+//   title: '散点图',
+//   icon: 'mdi-chart-bubble',
+//   option: {
+//     legend: {
+//       show: true
+//     },
+//     xAxis: {
+//       type: 'category',
+//       data: []
+//     },
+//     yAxis: {
+//       type: 'value'
+//     },
+//     series: [
+//       {
+//         data: [],
+//         type: 'scatter'
+//       }
+//     ]
+//   }
+// }

+ 1 - 1
src/views/dataDiagram/myChartDiagram/index.vue

@@ -139,7 +139,7 @@
 
     <m-dialog
       :visible.sync="showEditBox"
-      show-drawer
+      fullscreen
       flex-box
       :title="title + '图表'"
       :footer="false"

+ 3 - 0
src/views/dataFactory/components/graph.vue

@@ -101,6 +101,9 @@ export default {
     async init (data) {
       this.loading = true
       this.$refs.graphRef.setOptions(this.graphOptions, () => {
+        if (!this.$refs.graphRef || !this.$refs.graphRef.setJsonData) {
+          return
+        }
         this.$refs.graphRef.setJsonData(data, (graphInstance) => {
           this.loading = false
         })

+ 1 - 1
src/views/dataFactory/productionLineMonitor/index.vue

@@ -457,7 +457,7 @@ export default {
           try_number: item.try_number
         })
 
-        this.log.data = data.log.split('\n')
+        this.log.data = data.log.replace(/\\n/g, '\n').split('\n')
       } catch (error) {
         this.$snackbar.error(error)
       } finally {

+ 6 - 4
src/views/dataGovernance/dataIndicator/components/edit.vue

@@ -32,6 +32,7 @@
                 @input="updateContent"
               >
               </div>
+              <v-btn class="contenteditable-box-btn" color="primary" @click="handleCodeGenerate" :loading="codeLoading">代码生成</v-btn>
               <div class="labelTxt" :class="{'active': indicator}">指标计算规则</div>
             </div>
           </div>
@@ -45,9 +46,6 @@
               v-model="pythonCode"
               label="指标计算代码段"
             >
-              <template #append>
-                <v-btn class="ml-5" small color="primary" @click="handleCodeGenerate" :loading="codeLoading">代码生成</v-btn>
-              </template>
             </v-textarea>
           </div>
         </div>
@@ -372,6 +370,11 @@ export default {
   $color: #1976D2;
   position: relative;
   height: 100%;
+  &-btn {
+    position: absolute;
+    right: 10px;
+    bottom: 10px;
+  }
   .contenteditable {
     border: 1px solid #999;
     padding: 12px;
@@ -447,5 +450,4 @@ export default {
     z-index:-1;
   }
 }
-
 </style>

+ 26 - 3
src/views/dataGovernance/dataIndicator/components/editBase.vue

@@ -1,6 +1,9 @@
 <template>
   <div>
     <m-form ref="form" :items="formItems" v-model="formValues">
+      <template #en_name>
+        <v-btn color="primary" class="ml-3" :loading="translateLoading" @click="getTranslate">翻译</v-btn>
+      </template>
       <template #childrenId="{ item }">
         <search-nodes v-model="item.value" v-bind="item.options" :search-value="item.search"></search-nodes>
       </template>
@@ -17,6 +20,7 @@ import {
   frequency,
   sensitivity
 } from '@/utils/dataGovernance'
+import { getTranslate } from '@/api'
 import SearchNodes from '../../components/searchNodes.vue'
 import { api } from '@/api/dataGovernance'
 export default {
@@ -49,6 +53,7 @@ export default {
         current: 1
       },
       total: 0,
+      translateLoading: false,
       loading: false
     }
   },
@@ -56,7 +61,8 @@ export default {
     formItems () {
       return [
         { type: 'text', key: 'name', label: '请输入名称 *', rules: [v => !!v || '请输入名称'] },
-        { type: 'autocomplete', key: 'category', label: '请选择分类 *', rules: [v => !!v || '请选择分类'], items: metadata },
+        { type: 'text', key: 'en_name', label: '请输入英文名称 *', rules: [v => !!v || '请输入英文名称'], slotName: 'en_name' },
+        { type: 'autocomplete', key: 'category', label: '请选择分类 *', rules: [v => !!v || '请选择分类'], items: [...metadata] },
         { type: 'text', key: 'organization', label: '请输入所属机构 *', rules: [v => !!v || '请输入所属机构'] },
         { type: 'text', key: 'leader', label: '请输入负责人 *', rules: [v => !!v || '请输入负责人'] },
         {
@@ -68,8 +74,8 @@ export default {
             attach: true
           }
         },
-        { type: 'autocomplete', key: 'frequency', label: '请选择更新频率 *', rules: [v => !!v || '请选择更新频率'], items: frequency },
-        { type: 'autocomplete', key: 'data_sensitivity', label: '请选择数据敏感度 *', rules: [v => !!v || '请选择数据敏感度'], items: sensitivity },
+        { type: 'autocomplete', key: 'frequency', label: '请选择更新频率 *', rules: [v => !!v || '请选择更新频率'], items: [...frequency] },
+        { type: 'autocomplete', key: 'data_sensitivity', label: '请选择数据敏感度 *', rules: [v => !!v || '请选择数据敏感度'], items: [...sensitivity] },
         { type: 'autocomplete', key: 'tag', label: '请选择标签', itemText: 'name', itemValue: 'id', items: this.tagItems },
         { type: 'text', key: 'describe', label: '请输入描述' },
         {
@@ -129,6 +135,23 @@ export default {
         this.$snackbar.error(error)
       }
     },
+    async getTranslate () {
+      if (!this.formValues.name) {
+        this.$snackbar.error('请输入名称')
+        return
+      }
+      this.translateLoading = true
+      try {
+        const { data } = await getTranslate({
+          node_name: this.formValues.name
+        })
+        this.formValues.en_name = data.translated
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.translateLoading = false
+      }
+    },
     getValue () {
       if (!this.$refs.form.validate()) {
         return

+ 1 - 1
src/views/dataGovernance/dataIndicator/index.vue

@@ -38,7 +38,7 @@
     <m-dialog
       :visible.sync="show"
       :title="title"
-      :show-drawer="true"
+      :fullscreen="true"
       body-style="display: flex;padding: 0;"
       :footer="false"
     >

+ 90 - 12
src/views/dataGovernance/dataModules/components/edit.vue

@@ -59,6 +59,7 @@
                       </th>
                       <th class="text-right th-title">
                         操作
+                        <EditMetadataAdd @use="onUse"></EditMetadataAdd>
                         <v-btn
                           v-if="selected.length"
                           small
@@ -69,7 +70,7 @@
                     </tr>
                   </thead>
                   <tbody>
-                    <tr v-if="selected.length === 0" style="pointer-events: none;" class="text-center grey--text">
+                    <tr v-if="!selected.length" style="pointer-events: none;" class="text-center grey--text">
                       <td colspan="5">没有数据,可通过左侧数据资源进行选择</td>
                     </tr>
                     <tr
@@ -123,6 +124,67 @@
                         </v-btn>
                       </td>
                     </tr>
+                    <!-- <tr
+                      v-for="(item, index) in addItems"
+                      :key="index"
+                    >
+                      <td>
+                        <v-text-field
+                          label="元数据名称"
+                          v-model="item.name"
+                          outlined dense hide-details></v-text-field>
+                      </td>
+                      <td>
+                        <v-text-field v-model="item.data_type" outlined dense hide-details></v-text-field></td>
+                      <td>
+                        <m-editTable
+                          :text="item.master_data?.name || '--'"
+                          :init="originInit"
+                          item-label="title"
+                          item-value="id"
+                          @use="$el => originSave($el, item)"
+                        >
+                          <template #title>
+                            [{{ item.master_data?.name }}] 主数据选择
+                          </template>
+                        </m-editTable>
+                        <v-btn color="error" icon v-if="item.master_data?.name" @click="originClose(item)">
+                          <v-icon>mdi-close</v-icon>
+                        </v-btn>
+                      </td>
+                      <td>
+                        <m-editTable
+                          :text="item.data_standard?.name || '--'"
+                          :init="standardInit"
+                          item-label="name"
+                          item-value="id"
+                          @use="$el => standardSave($el, item)"
+                        >
+                          <template #title>
+                            [{{ item.name }}] 数据标准选择
+                          </template>
+                        </m-editTable>
+                        <v-btn color="error" icon v-if="item.data_standard?.name" @click="standardClose(item)">
+                          <v-icon>mdi-close</v-icon>
+                        </v-btn>
+                      </td>
+                      <td class="text-right">
+                        <v-btn
+                          color="error"
+                          text
+                          @click="handleRemove(index)"
+                        >
+                          移除
+                        </v-btn>
+                        <v-btn
+                          color="primary"
+                          text
+                          @click="handleRemove(index)"
+                        >
+                          保存
+                        </v-btn>
+                      </td>
+                    </tr> -->
                   </tbody>
                 </template>
               </v-simple-table>
@@ -171,10 +233,12 @@ import EditBase from './editBase'
 import MEditTable from '../../components/editTable'
 // import MForm from '@/components/Form/list'
 // import { metadataType } from '@/utils/dataGovernance'
+import EditMetadataAdd from './editMetadataAdd'
 import { api } from '@/api/dataGovernance'
 export default {
   name: 'modules-edit',
   components: {
+    EditMetadataAdd,
     MCard,
     MEditTable,
     EditBase,
@@ -197,6 +261,7 @@ export default {
       show: false,
       height: 0,
       selected: [], // 已选资源
+      addItems: [], // 新增资源
       sendBtn: [
         {
           icon: 'mdi-send-variant',
@@ -237,6 +302,24 @@ export default {
     })
   },
   methods: {
+    onUse (item) {
+      this.selected.push({
+        data_standard: {
+          id: null,
+          name: null
+        },
+        data_type: item.data_type,
+        en_name: item.en_name,
+        id: item.id,
+        master_data: {
+          name: null,
+          id: null
+        },
+        name: item.name,
+        originId: null,
+        originName: null
+      })
+    },
     handleSwitch () {
       this.selected.splice(0, this.selected.length)
     },
@@ -317,18 +400,13 @@ export default {
       this.$refs.dataList.initUsed()
     },
     // handleAdd () {
-    //   if (!this.$refs.form.validate()) {
-    //     return
-    //   }
-    //   const obj = this.formItems.options.reduce((r, v) => {
-    //     r[v.key] = v.value
-    //     return r
-    //   }, {})
     //   // 新增一个元数据
-    //   this.selected.push({
-    //     ...obj,
-    //     originId: null,
-    //     originName: null
+    //   this.addItems.push({
+    //     master_data: {},
+    //     data_standard: {},
+    //     name: null,
+    //     en_name: null,
+    //     data_type: null
     //   })
     // },
     async handleSubmit () {

+ 32 - 4
src/views/dataGovernance/dataModules/components/editBase.vue

@@ -1,6 +1,9 @@
 <template>
   <div>
     <m-form ref="form" :items="formItems" v-model="formValues">
+      <template #en_name>
+        <v-btn color="primary" class="ml-3" :loading="translateLoading" @click="getTranslate">翻译</v-btn>
+      </template>
       <template #childrenId="{ item }">
         <search-nodes v-model="item.value" v-bind="item.options" :search-value="item.search"></search-nodes>
       </template>
@@ -12,6 +15,7 @@
 
 import MForm from '@/components/MForm'
 // import { combinationTagsPage } from '@/api/dataBook'
+import { getTranslate } from '@/api'
 import {
   metadata,
   frequency,
@@ -53,6 +57,7 @@ export default {
       },
       total: 0,
       loading: false,
+      translateLoading: false,
       tagItems: []
     }
   },
@@ -60,8 +65,14 @@ export default {
     formItems () {
       return [
         { type: 'text', key: 'name', label: '请输入名称 *', rules: [v => !!v || '请输入名称'] },
-        { type: 'text', key: 'en_name', label: '请输入英文名称 *', rules: [v => !!v || '请输入英文名称'] },
-        { type: 'autocomplete', key: 'category', label: '请选择分类 *', rules: [v => !!v || '请选择分类'], items: metadata },
+        {
+          type: 'text',
+          key: 'en_name',
+          label: '请输入英文名称 *',
+          rules: [v => !!v || '请输入英文名称'],
+          slotName: 'en_name'
+        },
+        { type: 'autocomplete', key: 'category', label: '请选择分类 *', rules: [v => !!v || '请选择分类'], items: [...metadata] },
         { type: 'text', key: 'organization', label: '请输入所属机构 *', rules: [v => !!v || '请输入所属机构'] },
         { type: 'text', key: 'leader', label: '请输入负责人 *', rules: [v => !!v || '请输入负责人'] },
         {
@@ -73,8 +84,8 @@ export default {
             attach: true
           }
         },
-        { type: 'autocomplete', key: 'frequency', label: '请选择更新频率 *', rules: [v => !!v || '请选择更新频率'], items: frequency },
-        { type: 'autocomplete', key: 'data_sensitivity', label: '请选择数据敏感度 *', rules: [v => !!v || '请选择数据敏感度'], items: sensitivity },
+        { type: 'autocomplete', key: 'frequency', label: '请选择更新频率 *', rules: [v => !!v || '请选择更新频率'], items: [...frequency] },
+        { type: 'autocomplete', key: 'data_sensitivity', label: '请选择数据敏感度 *', rules: [v => !!v || '请选择数据敏感度'], items: [...sensitivity] },
         { type: 'autocomplete', key: 'tag', label: '请选择标签', itemText: 'name', itemValue: 'id', items: this.tagItems },
         { type: 'text', key: 'describe', label: '请输入描述' },
         {
@@ -131,6 +142,23 @@ export default {
         this.$snackbar.error(error)
       }
     },
+    async getTranslate () {
+      if (!this.formValues.name) {
+        this.$snackbar.error('请输入名称')
+        return
+      }
+      this.translateLoading = TextTrackCue
+      try {
+        const { data } = await getTranslate({
+          node_name: this.formValues.name
+        })
+        this.formValues.en_name = data.translated
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.translateLoading = false
+      }
+    },
     getValue () {
       if (!this.$refs.form.validate()) {
         return

+ 171 - 0
src/views/dataGovernance/dataModules/components/editMetadataAdd.vue

@@ -0,0 +1,171 @@
+<template>
+  <v-menu v-model="showMenu" offset-y nudge-bottom="10" allow-overflow :close-on-content-click="false" min-width="500" right @input="onInput">
+    <template v-slot:activator="{ on, attrs }">
+      <v-btn
+        class="ml-3"
+        color="primary"
+        small
+        v-on="on"
+        v-bind="attrs"
+      >
+        新增元数据
+      </v-btn>
+    </template>
+
+    <div class="pa-3 white">
+      <v-text-field
+        v-model="searchKey"
+        label="元数据"
+        placeholder="输入检索..."
+        dense
+        hide-details
+        clearable
+        outlined
+        @keyup.enter="onSearch"
+      >
+        <template #append>
+          <v-btn small icon color="primary" @click="onSearch">
+            <v-icon>mdi-magnify</v-icon>
+          </v-btn>
+        </template>
+      </v-text-field>
+      <div class="mt-3" style="max-height: 60vh; min-height: 200px; overflow: auto;">
+        <v-list dense>
+          <template v-for="(val, index) in items">
+            <v-list-item :key="val.name">
+              <template v-slot:default>
+                <v-list-item-content>
+                  <v-list-item-title>{{val.name}} [{{ val.data_type }}]</v-list-item-title>
+                </v-list-item-content>
+                <v-list-item-action>
+                  <v-btn text color="primary" small @click="onUse(val)">点击使用</v-btn>
+                </v-list-item-action>
+              </template>
+            </v-list-item>
+            <v-divider
+              v-if="index < items.length - 1"
+              :key="index"
+            ></v-divider>
+          </template>
+        </v-list>
+        <div class="d-flex align-center justify-center flex-column">
+          <v-progress-circular
+            v-if="loading"
+            indeterminate
+            color="primary"
+          ></v-progress-circular>
+          <v-btn
+            v-else
+            :disabled="total <= items.length"
+            text
+            color="primary"
+            @click="onMore"
+          >
+            <template v-if="total <= items.length">没有更多了</template>
+            <template v-else>查看更多</template>
+          </v-btn>
+          <div class="d-flex align-center text-body-2">
+            没有想要的元数据?
+            <v-btn
+              text
+              color="primary"
+              @click="show = true"
+            >新增一个元数据</v-btn>
+          </div>
+        </div>
+      </div>
+    </div>
+    <edit-dialog :visible.sync="show" title="新增元数据" @submit="onSubmit">
+      <MetadataEdit v-if="show" v-loading="submitLoading" ref="form" class="mt-5" :item-data="{ name: searchKey }"></MetadataEdit>
+    </edit-dialog>
+  </v-menu>
+</template>
+
+<script>
+import MetadataEdit from '../../metadata/components/edit'
+import EditDialog from '@/components/Dialog'
+import { api } from '@/api/dataGovernance'
+export default {
+  name: 'editMetadataAdd',
+  components: {
+    MetadataEdit,
+    EditDialog
+  },
+  data () {
+    return {
+      showMenu: false,
+      submitLoading: false,
+      show: false,
+      pageInfo: {
+        size: this.size,
+        current: 1
+      },
+      total: 0,
+      loading: false,
+      items: [],
+      searchKey: null
+    }
+  },
+  methods: {
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await api.getMetaDataList({ ...this.pageInfo, name: this.searchKey })
+        this.total = data.total
+        this.items.push(...data.records)
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    onInput (bool) {
+      if (!bool) {
+        return
+      }
+      this.searchKey = null
+      this.onSearch()
+    },
+    onMore () {
+      this.pageInfo.current++
+      this.init()
+    },
+    onSearch () {
+      this.pageInfo.current = 1
+      this.items = []
+      this.total = 0
+      this.init()
+    },
+    onUse (val) {
+      this.$emit('use', val)
+    },
+    async onSubmit () {
+      const obj = this.$refs.form.getValue()
+      if (!obj) {
+        return
+      }
+      try {
+        this.submitLoading = true
+        const { data } = await api.metadataAdd(obj)
+        this.$snackbar.success('操作成功')
+        this.showMenu = false
+        this.show = false
+        this.$emit('use', {
+          data_type: data.data_type,
+          en_name: data.en_name?.[0] ?? null,
+          id: data.id,
+          name: data.name
+        })
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.submitLoading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 1 - 1
src/views/dataGovernance/dataModules/index.vue

@@ -42,7 +42,7 @@
     <edit-dialog
       :visible.sync="show"
       :title="title"
-      :show-drawer="true"
+      :fullscreen="true"
       body-style="display: flex;padding: 0;"
       :footer="false"
     >

+ 189 - 26
src/views/dataGovernance/dataProcess/components/edit.vue

@@ -1,7 +1,9 @@
 <template>
-  <div class="d-flex fullscreen">
-    <v-card style="width: 400px;" class="mr-3 fullHeight"  v-loading="loading">
-      <v-banner>方案列表</v-banner>
+  <div class="d-flex fullscreen" v-loading="loading">
+    <v-card style="width: 400px;" class="mr-3 fullHeight">
+      <v-banner single-line>
+        <div class="py-2 title">方案列表</div>
+      </v-banner>
       <v-list>
         <v-list-item
           v-for="item in items"
@@ -25,27 +27,49 @@
     <v-card class="width-auto">
       <div class="pa-3 fullscreen">
         <div class="fullscreen d-flex flex-column">
-          <v-card outlined class="height-auto">
+          <div class="mt-3 d-flex">
+
+            <v-text-field
+              outlined
+              dense
+              hide-details
+              label="请输入来源表 *"
+              v-model="changeObj.source_table"
+              placeholder="请输入来源表"
+              :rules="[v => !!v || '请输入来源表']"
+            ></v-text-field>
+            <v-text-field
+              class="ml-3"
+              outlined
+              dense
+              hide-details
+              label="请输入目标表 *"
+              v-model="changeObj.target_table"
+              placeholder="请输入目标表"
+              :rules="[v => !!v || '请输入目标表']">
+            </v-text-field>
+          </div>
+          <v-card outlined class="height-auto mt-3">
             <Toolbar
                 style="border-bottom: 1px solid #ccc"
                 :editor="editor"
                 :defaultConfig="toolbarConfig"
                 :mode="mode"
-            >11</Toolbar>
+            ></Toolbar>
             <Editor
-                style="overflow-y: hidden; height: calc(100% - 100px);"
-                v-model="editorHtml"
+                style="overflow-y: hidden; height: calc(100% - 50px);"
+                v-model="changeObj.script_requirement"
                 :defaultConfig="editorConfig"
                 :mode="mode"
                 @onCreated="onCreated"
             />
-            <div class="d-flex align-end justify-end pa-3">
-              <v-btn color="primary">生成代码</v-btn>
-            </div>
           </v-card>
-          <div class="mt-3">
+          <div class="d-flex align-end justify-end pa-3">
+            <v-btn color="primary" @click="onGenerating">生成代码</v-btn>
+          </div>
+          <div>
             <v-textarea
-              v-model="code"
+              v-model="changeObj.script_content"
               outlined
               hide-details
               label="请输入执行代码"
@@ -56,9 +80,28 @@
         </div>
       </div>
     </v-card>
+    <v-card style="width: 400px;" class="ml-3 fullHeight d-flex flex-column">
+      <v-banner single-line>
+        <div class="py-2 title">基础信息</div>
+        <template #actions>
+          <v-btn
+            text
+            class="ml-3"
+            color="primary"
+            @click="handleSubmit"
+          >
+            <v-icon left>mdi-send-variant</v-icon>
+            确认提交
+          </v-btn>
+        </template>
+      </v-banner>
+      <div class="pt-3 height-auto overflow-y-auto">
+        <EditBase ref="editBaseRefs"></EditBase>
+      </div>
+    </v-card>
     <v-navigation-drawer
       v-model="drawer"
-      absolute
+      fixed
       temporary
       right
       width="800"
@@ -84,9 +127,11 @@
               <div class="fullscreen overflow-y-auto">
                 <v-tabs-items v-model="tab">
                   <v-tab-item>
-                    <div v-html="ruleItem.calculateFormulas[0]?.content"></div>
+                    <div class="text-right mb-3"><v-btn color="primary" @click="onUse">使用规则</v-btn></div>
+                    <div v-html="ruleItem.calculateFormulas?.[0]?.content"></div>
                   </v-tab-item>
                   <v-tab-item class="pa-3">
+                    <div class="text-right mb-3"><v-btn color="primary" @click="onUseParam">使用参数</v-btn></div>
                     <v-expansion-panels
                       v-model="panelActive"
                       multiple
@@ -150,15 +195,25 @@
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import '@wangeditor/editor/dist/css/style.css'
 // import NonePage from '@/components/Common/empty.vue'
+import EditBase from './editBase.vue'
 import axios from 'axios'
+import {
+  api
+} from '@/api/dataGovernance'
 export default {
   name: 'editPage',
-  components: { Editor, Toolbar },
+  components: { Editor, Toolbar, EditBase },
+  props: {
+    itemData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
   data () {
     return {
       drawer: false,
       editor: null,
-      editorHtml: null,
+      // editorHtml: null,
       toolbarConfig: {
         toolbarKeys: [
           'headerSelect',
@@ -170,8 +225,6 @@ export default {
       },
       editorConfig: { placeholder: '请输入内容...' },
       mode: 'default', // or 'simple'
-
-      code: null,
       loading: false,
       items: [],
       rulesItems: [],
@@ -179,7 +232,13 @@ export default {
       tab: 0,
       panelActive: [],
       clickItem: null,
-      clickId: null
+      clickId: null,
+      changeObj: {
+        script_content: '',
+        script_requirement: '',
+        source_table: null,
+        target_table: null
+      }
     }
   },
   computed: {
@@ -193,8 +252,13 @@ export default {
       return this.clickItem[this.active]
     }
   },
-  created () {
-    this.init()
+  async created () {
+    this.loading = true
+    if (Object.keys(this.itemData).length) {
+      await this.getDetails()
+    }
+    await this.getList()
+    this.loading = false
   },
   beforeDestroy () {
     const editor = this.editor
@@ -205,9 +269,35 @@ export default {
     onCreated (editor) {
       this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
     },
-    async init () {
+    onUse () {
+      this.changeObj.script_requirement += `\n${this.ruleItem.calculateFormulas?.[0]?.content}`
+    },
+    onUseParam () {
+      const str = this.ruleItem.calculateConfigurations.reduce((t, v) => {
+        return t + `<p>${v.name}=${v.value}</p>`
+      }, '\n')
+      this.changeObj.script_requirement += str
+      console.log(str)
+    },
+    // 详情
+    async getDetails () {
+      try {
+        const { data } = await api.getDataFlowDetails(this.itemData.id)
+        this.$nextTick(() => {
+          this.$refs.editBaseRefs.setValues(data)
+        })
+        this.changeObj = {
+          script_content: data.script_content,
+          script_requirement: data.script_requirement,
+          source_table: data.source_table,
+          target_table: data.target_table
+        }
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
+    async getList () {
       try {
-        this.loading = true
         const { data } = await this.fetchData('/op/base/performance/solution/page', {
           page: {
             size: 999,
@@ -218,8 +308,6 @@ export default {
       } catch (err) {
         this.items = []
         this.$snackbar.error(err)
-      } finally {
-        this.loading = false
       }
     },
     fetchData (url, params) {
@@ -240,13 +328,84 @@ export default {
         }).catch(reject)
       })
     },
-    async onClick (e) {
+    async onGenerating () {
+      // const text = this.editor.getText()
+      if (!this.changeObj.script_requirement) {
+        this.$snackbar.warning('请输入内容')
+        return
+      }
+      if (!this.changeObj.source_table) {
+        this.$snackbar.warning('请先填写来源表')
+        return
+      }
+      if (!this.changeObj.target_table) {
+        this.$snackbar.warning('请先填写目标表')
+        return
+      }
+      // if
       this.loading = true
       try {
+        const { data } = await api.getDataFlowScript({
+          request_data: this.changeObj.script_requirement,
+          input: this.changeObj.source_table,
+          output: this.changeObj.target_table
+        })
+        // this.changeObj.script_requirement = text
+        this.changeObj.script_content = data.script_content
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async handleSubmit () {
+      const params = this.$refs.editBaseRefs.getValue()
+      if (!params) {
+        return
+      }
+      const { script_content: scriptContent, source_table: sourceTable, target_table: targetTable } = this.changeObj
+      if (!scriptContent) {
+        this.$snackbar.warning('请先生成脚本')
+        return
+      }
+      if (!sourceTable) {
+        this.$snackbar.warning('请先填写来源表')
+        return
+      }
+      if (!targetTable) {
+        this.$snackbar.warning('请先填写目标表')
+        return
+      }
+      const query = {
+        ...params,
+        ...this.changeObj
+      }
+      const isEdit = Object.keys(this.itemData).length > 0
+      try {
+        if (isEdit) {
+          await api.updateDataFlow(this.itemData.id, query)
+        } else {
+          await api.addDataFlow(query)
+        }
+        this.$snackbar.success(isEdit ? '更新成功' : '新增成功')
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
+    async onClick (e) {
+      try {
+        this.loading = true
         const { data } = await this.fetchData('/op/base/performance/solution/detail', {
           performanceSolutionId: e.performanceSolutionId
         })
         this.clickId = data.entity.performanceSolutionId
+        if (!data.performanceSolutionDetailRespCategoryVos.length) {
+          this.loading = false
+          this.$snackbar.warning('该方案暂无数据')
+          this.drawer = false
+          return
+        }
         this.clickItem = data.performanceSolutionDetailRespCategoryVos
         this.drawer = true
       } catch (error) {
@@ -260,6 +419,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+.title {
+  color: #1976D2;
+  font-weight: 600;
+}
 .fullscreen {
   height: 100%;
   width: 100%;

+ 191 - 0
src/views/dataGovernance/dataProcess/components/editBase.vue

@@ -0,0 +1,191 @@
+<template>
+  <div>
+    <m-form ref="form" :items="formItems" v-model="formValues">
+      <template #name_en>
+        <v-btn color="primary" class="ml-3" :loading="translateLoading" @click="getTranslate">翻译</v-btn>
+      </template>
+      <template #childrenId="{ item }">
+        <search-nodes v-model="item.value" v-bind="item.options" :search-value="item.search"></search-nodes>
+      </template>
+    </m-form>
+  </div>
+</template>
+
+<script>
+
+import { getTranslate } from '@/api'
+import MForm from '@/components/MForm'
+// import { combinationTagsPage } from '@/api/dataBook'
+import {
+  metadata,
+  frequency
+  // sensitivity
+} from '@/utils/dataGovernance'
+import SearchNodes from '../../components/searchNodes.vue'
+import { api } from '@/api/dataGovernance'
+export default {
+  name: 'edit-base',
+  props: {
+    itemData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  components: { MForm, SearchNodes },
+  data () {
+    return {
+      formValues: {
+        name_zh: null,
+        name_en: null,
+        category: '应用类',
+        leader: 'system',
+        organization: null,
+        script_type: 'sql',
+        update_mode: 'append',
+        frequency: '月',
+        // data_sensitivity: '低',
+        tag: null,
+        describe: null,
+        status: 'active'
+      },
+      tagItems: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0,
+      translateLoading: false,
+      loading: false
+    }
+  },
+  computed: {
+    formItems () {
+      return [
+        { type: 'text', key: 'name_zh', label: '名称 *', rules: [v => !!v || '请输入名称'] },
+        { type: 'text', key: 'name_en', label: '英文名称 *', rules: [v => !!v || '请输入英文名称'], slotName: 'name_en' },
+        { type: 'autocomplete', key: 'category', label: '请选择分类 *', rules: [v => !!v || '请选择分类'], items: [...metadata] },
+        { type: 'text', key: 'leader', label: '创建者 *', rules: [v => !!v || '请输入负责人'] },
+        { type: 'text', key: 'organization', label: '请输入所属机构 *', rules: [v => !!v || '请输入所属机构'] },
+        {
+          type: 'autocomplete',
+          key: 'script_type',
+          label: '脚本类型 *',
+          rules: [v => !!v || '请选择脚本类型'],
+          items: [
+            { label: 'SQL', value: 'sql' },
+            { label: 'Python', value: 'python' },
+            { label: 'PythonScript', value: 'python_script' },
+            { label: 'SQLScript', value: 'sql_script' }
+          ]
+        },
+        {
+          type: 'autocomplete',
+          key: 'update_mode',
+          label: '更新模式 *',
+          rules: [v => !!v || '请选择更新模式'],
+          items: [
+            { label: '追加', value: 'append' },
+            { label: '全量更新', value: 'full_refresh' }
+          ]
+        },
+        { type: 'autocomplete', key: 'frequency', label: '更新频率 *', rules: [v => !!v || '请选择更新频率'], items: [...frequency] },
+        {
+          type: 'autocomplete',
+          key: 'tag',
+          label: '标签',
+          itemText: 'name',
+          itemValue: 'id',
+          items: this.tagItems
+        },
+        { type: 'text', key: 'describe', label: '描述' },
+        {
+          type: 'ifRadio',
+          key: 'status',
+          label: '启用',
+          width: 120,
+          items: [{ label: '是', value: 'active' }, { label: '否', value: 'inactive' }]
+        }
+      ]
+    }
+  },
+  created () {
+    this.init()
+    // if (!Object.keys(this.itemData).length) {
+    //   return
+    // }
+    // Object.keys(this.formValues).forEach(key => {
+    //   if (!Object.prototype.hasOwnProperty.call(this.itemData, key)) {
+    //     return
+    //   }
+    //   if (key === 'tag') {
+    //     this.formValues[key] = this.itemData.tag
+    //     return
+    //   }
+    //   this.formValues[key] = this.itemData[key]
+    // })
+  },
+  methods: {
+    async getTranslate () {
+      if (!this.formValues.name_zh) {
+        this.$snackbar.error('请输入名称')
+        return
+      }
+      this.translateLoading = true
+      try {
+        const { data } = await getTranslate({
+          node_name: this.formValues.name_zh
+        })
+        this.formValues.name_en = data.translated
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.translateLoading = false
+      }
+    },
+    setValues (item) {
+      this.formValues = {
+        name_zh: item.name_zh,
+        name_en: item.name_en,
+        category: item.category,
+        leader: item.leader,
+        organization: item.organization,
+        script_type: item.script_type,
+        update_mode: item.update_mode,
+        frequency: item.frequency,
+        tag: item.tag,
+        describe: item.describe,
+        status: item.status
+
+      }
+    },
+    async init () {
+      try {
+        this.loading = true
+        try {
+          const { data } = await api.dataLabelList({
+            ...this.pageInfo
+          })
+          this.tagItems = data.records
+          this.total = data.total
+        } catch (error) {
+          this.$snackbar.error(error)
+        } finally {
+          this.loading = false
+        }
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
+    getValue () {
+      if (!this.$refs.form.validate()) {
+        return
+      }
+      return this.formValues
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 54 - 15
src/views/dataGovernance/dataProcess/index.vue

@@ -11,14 +11,22 @@
       :show-select="false"
       :can-delete="false"
       @add="handleAdd"
-      @edit="handleEdit"
-      @delete="handleDelete"
       @pageHandleChange="pageHandleChange"
       @sort="handleSort"
     >
+      <template #status="{ item }">
+        <v-chip small :color="item.status === 'active' ? 'success' : 'error'">
+          {{ item.status === 'active' ? '启用' : '禁用' }}
+        </v-chip>
+      </template>
+      <template #actions="{ item }">
+        <v-btn color="primary" text class="mr-2" @click="onEdit(item)">编辑</v-btn>
+        <!-- <v-btn color="success" text class="mr-2"  @click="onRun(item)">运行</v-btn> -->
+        <v-btn color="error" text  @click="onDelete(item)">删除</v-btn>
+      </template>
     </m-table>
-    <m-dialog :title="title" :visible.sync="show" showDrawer @submit="handleSubmit">
-      <edit-page v-if="show" ref="edit" :item-data="itemData"></edit-page>
+    <m-dialog :title="title" :visible.sync="show" fullscreen :footer="false">
+      <edit-page v-if="show" ref="edit" :item-data="itemData" @success="show = false; init()"></edit-page>
     </m-dialog>
   </div>
 </template>
@@ -28,8 +36,9 @@ import MFilter from '@/components/Filter'
 import MTable from '@/components/List/table.vue'
 import MDialog from '@/components/Dialog'
 import EditPage from './components/edit'
+import { api } from '@/api/dataGovernance'
 export default {
-  name: 'template-name',
+  name: 'dataProcess',
   components: { MFilter, MTable, MDialog, EditPage },
   data () {
     return {
@@ -37,15 +46,23 @@ export default {
       show: false,
       filter: {
         list: [
-          { type: 'textField', value: '', label: '名称', key: 'name' },
-          { type: 'autocomplete', value: null, label: '选择', key: 'type', items: [] }
+          { type: 'textField', value: '', label: '关键词', key: 'title' }
         ]
       },
       queryData: {
-        name: null
+        title: null
       },
       headers: [
-        { text: '名称', align: 'start', value: 'name' }
+        { text: '中文名称', align: 'start', value: 'name_zh' },
+        { text: '英文名称', align: 'start', value: 'name_en' },
+        { text: '状态', align: 'center', value: 'status' },
+        { text: '分类', align: 'center', value: 'category' },
+        { text: '作者', align: 'center', value: 'leader' },
+        { text: '机构', align: 'center', value: 'organization' },
+        { text: '描述', align: 'start', value: 'describe' },
+        { text: '创建时间', align: 'start', value: 'created_at' },
+        { text: '更新时间', align: 'start', value: 'updated_at' },
+        { text: '操作', align: 'start', value: 'actions' }
       ],
       itemData: {},
       items: [],
@@ -66,7 +83,22 @@ export default {
     this.init()
   },
   methods: {
-    async init () {},
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await api.getDataFlowList({
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size,
+          search: this.queryData.title || undefined
+        })
+        this.items = data.list
+        this.total = data.pagination.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
     handleSearch (val) {
       Object.assign(this.queryData, val)
       this.pageInfo.current = 1
@@ -76,15 +108,15 @@ export default {
       this.itemData = {}
       this.show = true
     },
-    async handleEdit (item) {
+    async onEdit (item) {
       this.itemData = item
       this.show = true
     },
-    handleDelete (ids) {
-      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+    onDelete ({ id }) {
       this.$confirm('提示', '是否删除该选项')
         .then(async () => {
           try {
+            await api.deleteDataFlow(id)
             this.$snackbar.success('删除成功')
             this.init()
           } catch (error) {
@@ -92,6 +124,14 @@ export default {
           }
         })
     },
+    async onRun ({ id }) {
+      try {
+        await api.runDataFlow(id)
+        this.$snackbar.success('已启动运行')
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
     pageHandleChange (page) {
       this.pageInfo.current = page
       this.init()
@@ -99,8 +139,7 @@ export default {
     handleSort (val) {
       this.orders = val
       this.init()
-    },
-    handleSubmit () {}
+    }
   }
 }
 </script>

+ 0 - 1
src/views/dataGovernance/dataResource/components/Structure/index.vue

@@ -180,7 +180,6 @@ export default {
         }
       ],
       result: {},
-      showDrawer: false,
       ExcelHeight: 0,
       ExcelWidth: 0,
       showExcel: false,

+ 27 - 4
src/views/dataGovernance/dataResource/components/editBase.vue

@@ -1,6 +1,9 @@
 <template>
   <div>
     <m-form ref="form" :items="formItems" v-model="formValues">
+      <template #en_name>
+        <v-btn color="primary" class="ml-3" :loading="translateLoading" @click="getTranslate">翻译</v-btn>
+      </template>
     </m-form>
   </div>
 </template>
@@ -15,6 +18,7 @@ import {
 import {
   getDatasourceList
 } from '@/api/dataOrigin'
+import { getTranslate } from '@/api'
 import { api } from '@/api/dataGovernance'
 export default {
   name: 'edit-base',
@@ -51,6 +55,7 @@ export default {
         current: 1
       },
       loading: false,
+      translateLoading: false,
       labelItems: [],
       dataSourceItems: []
     }
@@ -68,14 +73,15 @@ export default {
           type: 'text',
           key: 'en_name',
           label: '请输入英文名称 *',
-          rules: [v => !!v || '请输入名称']
+          rules: [v => !!v || '请输入名称'],
+          slotName: 'en_name'
         },
         {
           type: 'autocomplete',
           key: 'category',
           label: '请选择分类 *',
           rules: [v => !!v || '请选择分类'],
-          items: metadata
+          items: [...metadata]
         },
         {
           type: 'text',
@@ -95,7 +101,7 @@ export default {
           label: '请选择更新频率 *',
           noAttach: true,
           rules: [v => !!v || '请选择更新频率'],
-          items: frequency
+          items: [...frequency]
         },
         {
           type: 'autocomplete',
@@ -103,7 +109,7 @@ export default {
           label: '请选择数据敏感度 *',
           noAttach: true,
           rules: [v => !!v || '请选择数据敏感度'],
-          items: sensitivity
+          items: [...sensitivity]
         },
         {
           type: 'ifRadio',
@@ -191,6 +197,23 @@ export default {
         this.$snackbar.error(error)
       }
     },
+    async getTranslate () {
+      if (!this.formValues.name) {
+        this.$snackbar.error('请输入名称')
+        return
+      }
+      this.translateLoading = true
+      try {
+        const { data } = await getTranslate({
+          node_name: this.formValues.name
+        })
+        this.formValues.en_name = data.translated
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.translateLoading = false
+      }
+    },
     getValue () {
       if (!this.$refs.form.validate()) {
         return

+ 1 - 1
src/views/dataGovernance/dataResource/index.vue

@@ -46,7 +46,7 @@
     <m-dialog
       :title="title"
       :visible.sync="show"
-      :show-drawer="true"
+      :fullscreen="true"
       body-style="display: flex;padding: 0;"
       :footer="false"
     >

+ 57 - 16
src/views/dataGovernance/metadata/components/edit.vue

@@ -1,11 +1,18 @@
 <template>
-  <m-form ref="form" class="mt-5" :items="formItems" v-model="formValues"></m-form>
+  <m-form ref="form" class="mt-5" :items="formItems" v-model="formValues">
+    <template #en_name>
+      <v-btn color="primary" class="ml-3" @click="getTranslate" :loading="translateLoading">翻译</v-btn>
+    </template>
+  </m-form>
 </template>
 
 <script>
 import MForm from '@/components/MForm'
+import {
+  getTranslate
+} from '@/api'
 
-import { metadata, metadataType } from '@/utils/dataGovernance'
+import { metadata } from '@/utils/dataGovernance'
 import { api } from '@/api/dataGovernance'
 
 export default {
@@ -19,7 +26,18 @@ export default {
   },
   data () {
     return {
-      formValues: {},
+      translateLoading: false,
+      formValues: {
+        name: null,
+        en_name: null,
+        category: null,
+        alias: null,
+        affiliation: null,
+        data_type: null,
+        tag: null,
+        describe: null,
+        status: 1
+      },
       pageInfo: {
         size: 50,
         current: 1
@@ -42,7 +60,8 @@ export default {
           type: 'text',
           key: 'en_name',
           label: '请输入英文名称 *',
-          rules: [v => !!v || '请输入英文名称']
+          rules: [v => !!v || '请输入英文名称'],
+          slotName: 'en_name'
         },
         {
           type: 'autocomplete',
@@ -50,7 +69,7 @@ export default {
           label: '请选择分类 *',
           noAttach: true,
           rules: [v => !!v || '请选择分类'],
-          items: metadata
+          items: [...metadata]
         },
         {
           type: 'text',
@@ -63,18 +82,23 @@ export default {
           label: '请输入制作单位'
         },
         {
-          type: 'autocomplete',
+          type: 'text',
           key: 'data_type',
           label: '请选择数据类型 *',
           col: 6,
-          noAttach: true,
-          rules: [v => !!v || '请选择数据类型'],
-          items: metadataType
+          rules: [v => !!v || '请选择输入类型']
+
+          // type: 'autocomplete',
+          // key: 'data_type',
+          // label: '请选择数据类型 *',
+          // col: 6,
+          // noAttach: true,
+          // rules: [v => !!v || '请选择数据类型'],
+          // items: [...metadataType]
         },
         {
           type: 'autocomplete',
           key: 'tag',
-          value: null,
           label: '请选择标签',
           col: 6,
           noAttach: true,
@@ -90,22 +114,22 @@ export default {
         {
           type: 'ifRadio',
           key: 'status',
-          value: 1,
           label: '启用',
           width: 120,
-          items: [{ label: '是', value: 1 }, { label: '否', value: 0 }]
+          items: [{ label: '是', value: true }, { label: '否', value: false }]
         }
       ]
     }
   },
   created () {
     this.init()
-    if (!this.itemData.id) {
+    if (!Object.keys(this.itemData).length) {
       return
     }
-    this.formItems.forEach(item => {
-      this.formValues[item.key] = this.itemData[item.key]
-    })
+    this.formValues = this.formItems.reduce((acc, cur) => {
+      acc[cur.key] = this.itemData[cur.key] ?? this.formValues[cur.key]
+      return acc
+    }, {})
   },
   methods: {
     async init () {
@@ -126,6 +150,23 @@ export default {
         this.$snackbar.error(error)
       }
     },
+    async getTranslate () {
+      if (!this.formValues.name) {
+        this.$snackbar.error('请输入名称')
+        return
+      }
+      this.translateLoading = true
+      try {
+        const { data } = await getTranslate({
+          node_name: this.formValues.name
+        })
+        this.formValues.en_name = data.translated
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.translateLoading = false
+      }
+    },
     getValue () {
       if (!this.$refs.form.validate()) {
         return

+ 0 - 3
src/views/dataGovernance/metadata/index.vue

@@ -34,19 +34,16 @@
       <template #actions="{ item }">
         <v-btn
           text
-          small
           :disabled="loading"
           color="primary"
           @click="handleEdit(item)">编辑</v-btn>
         <v-btn
           text
-          small
           :disabled="loading"
           color="error"
           @click="handleDelete(item.id)">删除</v-btn>
         <v-btn
           text
-          small
           :disabled="loading"
           :color="item.status ? 'error' : 'success'"
           @click="handleChangeStatus(item)"

+ 1 - 1
src/views/dataOrigin/unstructuredData/manualCollection/components/imageImportEdit.vue

@@ -1,5 +1,5 @@
 <template>
-  <m-dialog title="名片解析" :visible.sync="show" :showDrawer="id !== null" :footer="false" @close="handleClose">
+  <m-dialog title="名片解析" :visible.sync="show" :fullscreen="id !== null" :footer="false" @close="handleClose">
     <div class="fullBox box-1" :class="{ 'box-2': id }">
       <v-card class="upload-card d-flex flex-column align-center justify-center overflow-hidden" elevation="5">
         <template v-if="file">

+ 0 - 1
src/views/home/components/featureModule.vue

@@ -131,7 +131,6 @@ export default {
           rolesItem.push(ele)
           return
         }
-        // const meta = JSON.parse(ele.metastr)
         const meta = ele.meta
         // 处理快捷方式
         if (meta.homeType && meta.homeType === 2) {

+ 64 - 0
src/views/modelSystem/backup/backupCreate.vue

@@ -0,0 +1,64 @@
+<template>
+  <MDialog :visible.sync="show" title="创建备份" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </MDialog>
+</template>
+
+<script>
+import MForm from '@/components/MForm'
+import MDialog from '@/components/Dialog'
+import {
+  addPgBackup
+} from '@/api/dataChart'
+export default {
+  name: 'backupCreate',
+  components: { MDialog, MForm },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      formValues: {
+        db_connection: null
+      },
+      formItems: [
+        {
+          label: 'PostgreSQL连接字符串',
+          key: 'db_connection',
+          type: 'text',
+          placeholder: '请输入PostgreSQL连接字符串,不提供则从config.py获取'
+        }
+      ],
+      itemData: null
+    }
+  },
+  methods: {
+    open (item) {
+      this.itemData = item ?? null
+      this.show = true
+      this.formValues = {
+        db_connection: null
+      }
+    },
+    async handleSubmit () {
+      this.loading = true
+      try {
+        await addPgBackup({
+          ...this.formValues,
+          task_id: this.itemData?.task_id
+        })
+        this.$snackbar.success('创建备份成功')
+        this.$emit('success')
+        this.show = false
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 72 - 0
src/views/modelSystem/backup/backupDetails.vue

@@ -0,0 +1,72 @@
+<template>
+  <MDialog :visible.sync="show" title="文件列表" widthType="1">
+    <MTable
+      :loading="false"
+      elevation="0"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      :show-select="false"
+      :is-tools="false"
+    >
+      <template #actions="{ item }">
+        <v-btn text color="primary" @click="handleRestore(item)">恢复备份</v-btn>
+      </template>
+    </MTable>
+    <BackupDetailsConfirm ref="backupDetailsConfirmRefs" @success="$emit('success')"></BackupDetailsConfirm>
+  </MDialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MTable from '@/components/List/table'
+import BackupDetailsConfirm from './backupDetailsConfirm.vue'
+// import {
+//   pgBackupRestore
+// } from '@/api/dataChart'
+export default {
+  name: 'backupDetails',
+  components: {
+    MDialog,
+    MTable,
+    BackupDetailsConfirm
+  },
+  data () {
+    return {
+      show: false,
+      headers: [
+        { text: 'collection文件', align: 'start', value: 'collection_file' },
+        { text: 'collection文件大小', align: 'center', value: 'collection_size' },
+        { text: 'embedding文件', align: 'start', value: 'embedding_file' },
+        { text: 'embedding文件大小', align: 'center', value: 'embedding_size' },
+        { text: '备份日期', align: 'start', value: 'backup_date' },
+        { text: '操作', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  methods: {
+    open (item) {
+      this.itemData = item
+      this.items = item.backups
+      this.total = item.backups.length
+      this.show = true
+    },
+    handleRestore (item) {
+      this.$refs.backupDetailsConfirmRefs.open(item, this.itemData)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 91 - 0
src/views/modelSystem/backup/backupDetailsConfirm.vue

@@ -0,0 +1,91 @@
+<template>
+  <MDialog :visible.sync="show" title="恢复确认" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </MDialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  pgBackupRestore
+} from '@/api/dataChart'
+export default {
+  name: 'backupDetailsConfirm',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      formValues: {
+        tables: ['langchain_pg_collection', 'langchain_pg_embedding'],
+        db_connection: null,
+        truncate_before_restore: false
+      },
+      formItems: [
+        {
+          label: '恢复表名',
+          type: 'autocomplete',
+          key: 'tables',
+          multiple: true,
+          noAttach: true,
+          items: ['langchain_pg_collection', 'langchain_pg_embedding']
+        },
+        {
+          label: 'PostgreSQL连接字符串',
+          type: 'text',
+          key: 'db_connection'
+        },
+        {
+          label: '清空pgvecror目标表',
+          type: 'ifRadio',
+          key: 'truncate_before_restore',
+          width: '140',
+          items: [
+            { label: '是', value: true },
+            { label: '否', value: false }
+          ]
+        }
+      ],
+      itemData: {},
+      origin: {}
+    }
+  },
+  methods: {
+    open (item, origin) {
+      this.itemData = item
+      this.origin = origin
+      this.show = true
+      this.formValues = {
+        tables: ['langchain_pg_collection', 'langchain_pg_embedding'],
+        db_connection: null,
+        truncate_before_restore: false
+      }
+    },
+    async handleSubmit () {
+      this.loading = true
+      try {
+        await pgBackupRestore({
+          backup_path: this.origin.relative_path,
+          timestamp: this.itemData.timestamp,
+          ...this.formValues
+        })
+        this.$snackbar.success('恢复成功')
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 135 - 0
src/views/modelSystem/backup/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div class="pa-3 white">
+    <m-search :items="searchItems" v-model="searchValues" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      :show-select="false"
+      :is-tools="false"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #navBtn>
+        <v-btn elevation="5" class="buttons" color="primary" rounded @click="handleAdd">
+          <v-icon left>mdi-plus</v-icon>
+          创建备份
+        </v-btn>
+      </template>
+      <template #actions="{ item }">
+        <v-btn text color="primary" @click="handleAdd(item)">创建备份</v-btn>
+        <v-btn text color="primary" @click="handleDetails(item)">查看备份文件</v-btn>
+      </template>
+    </m-table>
+    <BackupDetails ref="backupDetailsRefs" @success="init"></BackupDetails>
+    <BackupCreate ref="backupCreateRefs" @success="init"></BackupCreate>
+  </div>
+</template>
+
+<script>
+import MSearch from '@/components/MSearch'
+import MTable from '@/components/List/table.vue'
+import BackupDetails from './backupDetails.vue'
+import BackupCreate from './backupCreate.vue'
+import {
+  getPgBackupList
+} from '@/api/dataChart'
+export default {
+  name: 'pgBackup',
+  components: { MSearch, MTable, BackupDetails, BackupCreate },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      searchValues: {},
+      searchItems: [
+        {
+          type: 'autocomplete',
+          label: '备份目录',
+          key: 'global_only',
+          items: [
+            { label: '仅查询全局备份目录', value: true },
+            { label: '不限备份目录', value: false }
+          ]
+        },
+        {
+          type: 'text',
+          label: '任务id',
+          key: 'task_id'
+        }
+      ],
+      queryData: {
+        name: null
+      },
+      headers: [
+        { text: '任务ID', align: 'start', value: 'task_id' },
+        { text: '类型', align: 'center', value: 'type' },
+        { text: '路径', align: 'start', value: 'relative_path' },
+        { text: '操作', value: 'actions' }
+      ],
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await getPgBackupList(this.searchValues)
+        this.items = data.backup_locations
+        this.total = data.summary.total_locations
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleAdd (item) {
+      this.$refs.backupCreateRefs.open(item)
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    },
+    handleDetails (item) {
+      this.$refs.backupDetailsRefs.open(item)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 370 - 0
src/views/modelSystem/modelHistory/index.vue

@@ -0,0 +1,370 @@
+<template>
+  <div class="white content">
+    <div class="white">
+      <div v-loading="loading">
+        <v-banner class="mb-3">
+          Redis
+          <template v-slot:actions>
+            <v-btn
+              text
+              color="primary"
+              @click="getConversationsStatus"
+            >
+              <v-icon left>mdi-refresh</v-icon>
+              刷新
+            </v-btn>
+            <v-btn
+              text
+              color="deep-purple accent-4"
+              @click="onClearRedisCache({ clear_all_agent_data: true })"
+            >
+              <v-icon left>mdi-trash-can-outline</v-icon>
+              清空所有agent对话数据
+            </v-btn>
+            <v-btn
+              text
+              color="deep-purple accent-4"
+              @click="onClearRedisCache({ cleanup_invalid_refs: true })"
+            >
+              <v-icon left>mdi-trash-can-outline</v-icon>
+              清理无效引用
+            </v-btn>
+            <!-- <v-btn
+              text
+              color="deep-purple accent-4"
+              @click="onClearRedisCache"
+            >
+              <v-icon left>mdi-trash-can-outline</v-icon>
+              清除所有对话缓存
+            </v-btn> -->
+          </template>
+        </v-banner>
+        <v-container>
+          <v-row >
+            <v-col
+              v-for="elevation in elevations"
+              :key="elevation.value"
+              cols="3"
+            >
+              <v-card
+                class="pa-2"
+                tile
+                height="150"
+              >
+                <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+                  <div class="text-h3" :class="`${elevation.textColor}--text`">{{ itemData[elevation.value] }}</div>
+                  <div>{{ elevation.text }}</div>
+                </div>
+              </v-card>
+            </v-col>
+          </v-row>
+        </v-container>
+      </div>
+      <!-- <v-divider></v-divider> -->
+
+      <div v-loading="embLoading">
+        <v-banner class="mb-3">
+          embedding
+          <template v-slot:actions>
+            <v-btn
+              text
+              color="primary"
+              @click="getEmbeddingStats"
+            >
+              <v-icon left>mdi-refresh</v-icon>
+              刷新
+            </v-btn>
+            <v-btn
+              text
+              color="deep-purple accent-4"
+              @click="onClearEmbeddingCache"
+            >
+              <v-icon left>mdi-trash-can-outline</v-icon>
+              清除对话缓存
+            </v-btn>
+          </template>
+        </v-banner>
+        <v-container>
+          <v-row >
+            <v-col
+              v-for="elevation in embeddingElevations"
+              :key="elevation.value"
+              cols="3"
+            >
+              <v-card
+                class="pa-2"
+                tile
+                height="150"
+              >
+                <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+                  <div class="text-h3" :class="`${elevation.textColor}--text`">{{ embeddingData[elevation.value] }}</div>
+                  <div>{{ elevation.text }}</div>
+                </div>
+              </v-card>
+            </v-col>
+          </v-row>
+        </v-container>
+      </div>
+
+      <div v-loading="checkpointLoading">
+        <v-banner class="mb-3">
+          Checkpoint
+          <template v-slot:actions>
+            <v-btn
+              text
+              color="primary"
+              @click="getCheckpoint"
+            >
+              <v-icon left>mdi-refresh</v-icon>
+              刷新
+            </v-btn>
+            <v-btn
+              text
+              color="deep-purple accent-4"
+              @click="onClearCheckpoint"
+            >
+              <v-icon left>mdi-trash-can-outline</v-icon>
+              清除对话缓存
+            </v-btn>
+          </template>
+        </v-banner>
+        <v-container>
+          <v-row >
+            <v-col
+              v-for="elevation in checkpointElevations"
+              :key="elevation.value"
+              cols="3"
+            >
+              <v-card
+                class="pa-2"
+                tile
+                height="150"
+              >
+                <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+                  <div
+                    class="text-h3"
+                    :class="`${elevation.textColor}--text ${elevation.onClick ? 'cursor-pointer' : ''}`"
+                    @click="elevation.onClick && elevation.onClick(elevation, checkpointData)"
+                  >
+                    {{ checkpointData[elevation.value] }}
+                  </div>
+                  <div>{{ elevation.text }}</div>
+                </div>
+              </v-card>
+            </v-col>
+          </v-row>
+        </v-container>
+      </div>
+    </div>
+    <MDialog :visible.sync="show" title="设置保留checkpoint数量" @submit="handleSubmit" widthType="2">
+      <v-text-field
+        v-model="checkpointParam.keep_count"
+        dense
+        label="checkpoint数量"
+        placeholder="请输入每个线程保留的checkpoint数量"
+        outlined
+      ></v-text-field>
+    </MDialog>
+    <ModelDetails ref="userRefs" title="用户列表">
+      <template #actions="{ item }">
+        <v-btn
+          text
+          color="primary"
+          @click="onClearCheckpoint({
+            keep_count: 10,
+            user_id: item.user_id,
+            thread_id: undefined
+          })"
+        >清理Checkpoint</v-btn>
+      </template>
+    </ModelDetails>
+    <ModelDetails ref="threadRefs" title="线程列表">
+      <template #actions="{ item }">
+        <v-btn
+          text
+          color="primary"
+          @click="onClearCheckpoint({
+            keep_count: 10,
+            user_id: undefined,
+            thread_id: item.thread_id
+          })"
+        >清理Checkpoint</v-btn>
+      </template>
+    </ModelDetails>
+  </div>
+</template>
+
+<script>
+import {
+  getConversationsStatus,
+  clearConversation,
+  getEmbeddingStats,
+  clearEmbeddingCache,
+  getCheckpoint,
+  clearCheckpoint
+} from '@/api/dataChart'
+import MDialog from '@/components/Dialog'
+import ModelDetails from './modelDetails'
+export default {
+  name: 'modelHistory',
+  components: {
+    MDialog,
+    ModelDetails
+  },
+  data () {
+    return {
+      showUser: false,
+      show: false,
+      loading: false,
+      embLoading: false,
+      checkpointLoading: false,
+      elevations: [
+        { value: 'connected_clients', text: '客户端连接数', textColor: 'primary' },
+        { value: 'memory_usage_mb', text: 'redis占用内存(M)', textColor: 'primary' },
+        { value: 'total_conversations', text: '会话总数', textColor: 'primary' },
+        { value: 'total_users', text: '用户数', textColor: 'primary' }
+      ],
+      itemData: {},
+      embeddingElevations: [
+        { value: 'memory_usage_mb', text: 'embedding占用内存(M)', textColor: 'primary' },
+        { value: 'total_count', text: '总数', textColor: 'primary' }
+      ],
+      checkpointElevations: [
+        { value: 'total_users', text: '用户数量', textColor: 'primary', onClick: this.onClickUser },
+        { value: 'total_threads', text: '线程总数', textColor: 'primary', onClick: this.onClickThread },
+        { value: 'total_checkpoints', text: 'checkpoints总数', textColor: 'primary' }
+      ],
+      embeddingData: {},
+      checkpointData: {},
+      checkpointParam: {
+        keep_count: 10,
+        user_id: undefined,
+        thread_id: undefined
+      }
+    }
+  },
+  created () {
+    this.getConversationsStatus()
+    this.getEmbeddingStats()
+    this.getCheckpoint()
+  },
+  methods: {
+    onClickUser (item, itemData) {
+      this.$refs.userRefs.open([
+        { text: '用户ID', value: 'user_id' },
+        { text: '线程总数', value: 'thread_count' },
+        { text: 'checkpoints数', value: 'total_checkpoints' },
+        { text: '操作', value: 'actions' }
+      ], itemData.users)
+    },
+    onClickThread (item, itemData) {
+      this.$refs.threadRefs.open([
+        { text: '线程ID', value: 'thread_id' },
+        { text: 'checkpoints数', value: 'checkpoint_count' },
+        { text: '用户ID', value: 'user_id' },
+        { text: '操作', value: 'actions' }
+      ], itemData.users.reduce((acc, cur) => {
+        acc.push(...cur.threads.map(e => {
+          return { ...e, user_id: cur.user_id }
+        }))
+        return acc
+      }, []))
+    },
+    async getConversationsStatus () {
+      this.loading = true
+      try {
+        const { data } = await getConversationsStatus()
+        this.itemData = { ...data, ...data.redis_info }
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async getEmbeddingStats () {
+      this.embLoading = true
+      try {
+        const { data } = await getEmbeddingStats()
+        this.embeddingData = data
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.embLoading = false
+      }
+    },
+    async getCheckpoint () {
+      this.checkpointLoading = true
+      try {
+        const { data } = await getCheckpoint()
+        this.checkpointData = data
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.checkpointLoading = false
+      }
+    },
+    onClearRedisCache (param) {
+      this.$confirm('提示', '确定执行清除吗?').then(async () => {
+        this.loading = true
+        try {
+          await clearConversation(param)
+          this.$snackbar.success('清除成功')
+          this.getConversationsStatus()
+        } catch (error) {
+          this.$snackbar.error(error)
+        } finally {
+          this.loading = false
+        }
+      })
+    },
+    onClearEmbeddingCache () {
+      this.$confirm('提示', '确定清除embedding缓存吗?').then(async () => {
+        this.loading = true
+        try {
+          await clearEmbeddingCache()
+          this.$snackbar.success('清除成功')
+          this.getEmbeddingStats()
+        } catch (error) {
+          this.$snackbar.error(error)
+        } finally {
+          this.loading = false
+        }
+      })
+    },
+    onClearCheckpoint (param = {
+      keep_count: 10,
+      user_id: undefined,
+      thread_id: undefined
+    }) {
+      this.show = true
+      this.checkpointParam = param
+    },
+    async handleSubmit () {
+      if (!this.checkpointParam.keep_count) {
+        this.$snackbar.error('请输入保留的checkpoint数量')
+        return
+      }
+      try {
+        this.checkpointParam.keep_count = Number(this.checkpointParam.keep_count)
+        const { data } = await clearCheckpoint(this.checkpointParam)
+        this.$snackbar.success(data.response)
+        this.show = false
+        this.getCheckpoint()
+        this.$refs.userRefs.close()
+        this.$refs.threadRefs.close()
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+  font-size: 16px;
+}
+.cursor-pointer {
+  cursor: pointer;
+}
+</style>

+ 49 - 0
src/views/modelSystem/modelHistory/modelDetails.vue

@@ -0,0 +1,49 @@
+<template>
+  <MDialog :visible.sync="show" v-bind="$attrs" :footer="false">
+    <MTable
+      :headers="headers"
+      :items="items"
+      :show-select="false"
+      :is-tools="false"
+      :loading="false"
+      elevation="0"
+    >
+      <template #actions="{ item }">
+        <slot name="actions" :item="item"></slot>
+      </template>
+    </MTable>
+  </MDialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MTable from '@/components/List/table'
+export default {
+  name: 'modelDetails',
+  components: {
+    MDialog,
+    MTable
+  },
+  data () {
+    return {
+      show: false,
+      headers: [],
+      items: []
+    }
+  },
+  methods: {
+    open (header, items) {
+      this.headers = header
+      this.items = items
+      this.show = true
+    },
+    close () {
+      this.show = false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 173 - 0
src/views/modelSystem/modelQa/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="pa-3 white">
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      ref="tableRefs"
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      :is-tools="false"
+      @delete="handleDelete"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #navBtn>
+        <v-btn class="buttons" rounded color="primary" @click="handleAddTo()">添加到训练集</v-btn>
+      </template>
+      <template #actions="{ item }">
+        <v-btn color="primary" text @click="handleEdit(item)">编辑</v-btn>
+        <v-btn color="primary" text :disabled="item.is_in_training_data" @click="handleAddTo([item.id])">添加到训练集</v-btn>
+        <v-btn color="error" text @click="handleDelete(item.id)">删除</v-btn>
+      </template>
+      <template #is_thumb_up="{ item }">
+        <v-chip :color="item.is_thumb_up ? 'success' : 'error'" small>{{ item.is_thumb_up ? '赞同' : '不赞同' }}</v-chip>
+      </template>
+      <template #is_in_training_data="{ item }">
+        <v-chip :color="item.is_in_training_data ? 'success' : 'error'" small>{{ item.is_in_training_data ? '是' : '否' }}</v-chip>
+      </template>
+      <template #sql="{ item }">
+        <Tooltip :text="item.sql" width="300"></Tooltip>
+      </template>
+      <template #question="{ item }">
+        <Tooltip :text="item.question" width="300"></Tooltip>
+      </template>
+    </m-table>
+    <ModelQaEdit ref="modelQaEditRefs"></ModelQaEdit>
+  </div>
+</template>
+
+<script>
+import Tooltip from '@/components/Tooltip'
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import ModelQaEdit from './modelQaEdit.vue'
+import {
+  getFeedbackList,
+  deleteFeedback,
+  addFeedbackToTraining
+} from '@/api/dataChart'
+export default {
+  name: 'model-qa',
+  components: { MFilter, MTable, ModelQaEdit, Tooltip },
+  data () {
+    return {
+      loading: false,
+      filter: {
+        list: [
+          {
+            type: 'autocomplete',
+            value: null,
+            label: '赞同与否',
+            key: 'is_thumb_up',
+            items: [
+              { label: '赞同', value: true },
+              { label: '不赞同', value: false }
+            ]
+          },
+          {
+            label: '创建时间',
+            type: 'datePicker',
+            key: 'create_time',
+            value: null,
+            dateType: 'date',
+            placeholder: '请选择创建时间',
+            clearable: true
+          }
+        ]
+      },
+      queryData: {},
+      headers: [
+        { text: '问题', align: 'start', value: 'question' },
+        { text: 'sql', align: 'start', value: 'sql' },
+        { text: '反馈', align: 'center', value: 'is_thumb_up' },
+        { text: '是否已加入训练数据', align: 'center', value: 'is_in_training_data' },
+        { text: '创建时间', align: 'start', value: 'create_time' },
+        { text: '操作', align: 'start', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      try {
+        this.loading = true
+        const { data } = await getFeedbackList({
+          ...this.queryData,
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size
+        })
+        this.items = data.records
+        this.total = data.pagination.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    async handleEdit (item) {
+      this.$refs.modelQaEditRefs.open(item)
+    },
+    handleAddTo (ids) {
+      console.log(ids)
+      const feedbackIds = ids ?? this.$refs.tableRefs.ids
+      console.log(feedbackIds)
+      if (!feedbackIds.length) {
+        this.$snackbar.warning('请选择需要添加的数据')
+        return
+      }
+      this.$confirm('提示', '是否添加到训练集').then(async _ => {
+        try {
+          await addFeedbackToTraining({ feedback_ids: feedbackIds })
+          this.$snackbar.success('添加成功')
+          this.init()
+        } catch (error) {
+          this.$snackbar.error('添加失败')
+        }
+      })
+    },
+    handleDelete (ids, item) {
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            await deleteFeedback({ id: item.feedback_id })
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    },
+    handleSubmit () {}
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 83 - 0
src/views/modelSystem/modelQa/modelQaEdit.vue

@@ -0,0 +1,83 @@
+<template>
+  <m-dialog title="编辑" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MForm from '@/components/MForm'
+import MDialog from '@/components/Dialog'
+import { updateFeedback } from '@/api/dataChart'
+export default {
+  name: 'modelQaEdit',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      formValues: {},
+      formItems: [
+        {
+          label: '问题',
+          key: 'question',
+          type: 'textarea'
+        },
+        {
+          label: 'SQL',
+          key: 'sql',
+          type: 'textarea',
+          rows: 10
+        },
+        {
+          label: '用户反馈',
+          key: 'is_thumb_up',
+          type: 'ifRadio',
+          items: [
+            {
+              label: '赞同',
+              value: true
+            },
+            {
+              label: '不赞同',
+              value: false
+            }
+          ]
+        }
+      ]
+    }
+  },
+  methods: {
+    open (item) {
+      this.formValues = {
+        question: item.question,
+        sql: item.sql,
+        is_thumb_up: item.is_thumb_up,
+        id: item.id
+      }
+      this.show = true
+    },
+    async handleSubmit () {
+      if (!this.$refs.form.validate()) {
+        return
+      }
+      this.loading = true
+      const { id, ...params } = this.formValues
+      try {
+        await updateFeedback(id, params)
+        this.$snackbar.success('保存成功')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 84 - 0
src/views/modelSystem/modelStatistics/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="content white">
+    <v-banner class="mb-3">
+      反馈统计
+      <template v-slot:actions>
+        <v-btn
+          text
+          color="primary"
+          @click="init"
+        >
+          <v-icon left>mdi-refresh</v-icon>
+          刷新
+        </v-btn>
+      </template>
+    </v-banner>
+    <v-container v-loading="loading">
+      <v-row >
+        <v-col
+          v-for="elevation in elevations"
+          :key="elevation.value"
+          cols="3"
+        >
+          <v-card
+            class="pa-2"
+            tile
+            height="150"
+          >
+            <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+              <div class="text-h3" :class="`${elevation.textColor}--text`">{{ itemData[elevation.value] }}</div>
+              <div>{{ elevation.text }}</div>
+            </div>
+          </v-card>
+        </v-col>
+      </v-row>
+
+    </v-container>
+  </div>
+</template>
+
+<script>
+import {
+  getFeedbackStats
+} from '@/api/dataChart'
+export default {
+  name: 'modelStatistics',
+  data () {
+    return {
+      elevations: [
+        { text: '总反馈数', value: 'total_feedback', textColor: 'primary' },
+        { text: '正向反馈数', value: 'positive_feedback', textColor: 'success' },
+        { text: '负向反馈数', value: 'negative_feedback', textColor: 'error' },
+        { text: '已训练反馈数', value: 'trained_feedback', textColor: 'indigo' },
+        { text: '未训练反馈数', value: 'untrained_feedback', textColor: 'warning' },
+        { text: '正向反馈率(%)', value: 'positive_rate', textColor: 'primary' },
+        { text: '训练覆盖率(%)', value: 'training_rate', textColor: 'primary' }
+      ],
+      itemData: {},
+      loading: false
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await getFeedbackStats()
+        this.itemData = data
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+  font-size: 16px;
+}
+</style>

+ 186 - 0
src/views/modelSystem/modelTrain/index.vue

@@ -0,0 +1,186 @@
+<template>
+  <div class="pa-3 white">
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      item-key="task_id"
+      @add="handleAdd"
+      @delete="handleDelete"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #status="{item}">
+        <v-chip :color="item.status === 'failed' ? 'error' : 'success'" small>{{ item.status }}</v-chip>
+      </template>
+      <template #actions="{ item }">
+        <template v-if="item.directory_exists">
+          <v-btn color="success" text @click="handleExecute(item.task_id)">执行</v-btn>
+          <v-btn color="primary" text @click="handleFindExecute(item.task_id)">执行进度</v-btn>
+          <v-btn color="primary" text @click="handleTable(item.task_id)">表名清单</v-btn>
+          <v-btn color="primary" text @click="handleFiles(item.task_id)">任务文件</v-btn>
+          <v-btn color="primary" text @click="handleLog(item.task_id)">运行日志</v-btn>
+          <v-btn color="error" text @click="handleDelete([item.task_id])">删除</v-btn>
+        </template>
+        <template v-else>
+          <v-chip small>已删除</v-chip>
+        </template>
+      </template>
+    </m-table>
+    <ModelTrainEdit ref="modelTrainEditRefs" @success="init"></ModelTrainEdit>
+    <ModelTrainStatus ref="modelTrainStatusRefs"></ModelTrainStatus>
+    <ModelTrainTable ref="modelTrainTableRefs"></ModelTrainTable>
+    <ModelTrainLog ref="modelTrainLogRefs"></ModelTrainLog>
+    <ModelTrainFiles ref="modelTrainFilesRefs"></ModelTrainFiles>
+    <!-- <ModelTrainStep ref="modelTrainStepRefs"></ModelTrainStep> -->
+  </div>
+</template>
+
+<script>
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import ModelTrainEdit from './modelTrainEdit.vue'
+import ModelTrainStatus from './modelTrainStatus.vue'
+import ModelTrainTable from './modelTrainTable.vue'
+import ModelTrainLog from './modelTrainLog.vue'
+import ModelTrainFiles from './modelTrainFiles.vue'
+import {
+  // createDataTasks,
+  executeDataTasks,
+  getDataTasksList,
+  deleteDataTasks
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrain',
+  components: {
+    MFilter,
+    MTable,
+    ModelTrainEdit,
+    ModelTrainStatus,
+    ModelTrainTable,
+    ModelTrainLog,
+    ModelTrainFiles
+  },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      filter: {
+        list: [
+          { type: 'textField', value: '', label: '任务名称', key: 'task_name' }
+        ]
+      },
+      queryData: {
+        task_name: null
+      },
+      headers: [
+        { text: '任务ID', align: 'start', value: 'task_id' },
+        { text: '任务名称', align: 'start', value: 'task_name' },
+        { text: '数据库名称', align: 'start', value: 'db_name' },
+        { text: '上下文', align: 'start', value: 'business_context' },
+        { text: '运行状态', align: 'center', value: 'status' },
+        { text: '创建者', align: 'center', value: 'created_by' },
+        { text: '操作', align: 'start', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {
+      this.loading = true
+      try {
+        const { data } = await getDataTasksList({
+          ...this.queryData,
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size
+        })
+        this.items = data.tasks
+        this.total = data.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleAdd () {
+      this.$refs.modelTrainEditRefs.open()
+    },
+    // handleEdit (item) {
+    //   this.itemData = item
+    //   this.show = true
+    // },
+    handleFindExecute (id) {
+      this.$refs.modelTrainStatusRefs.open(id)
+    },
+    handleTable (id) {
+      this.$refs.modelTrainTableRefs.open(id)
+    },
+    // 查看任务文件
+    handleFiles (id) {
+      this.$refs.modelTrainFilesRefs.open(id)
+    },
+    handleLog (id) {
+      this.$refs.modelTrainLogRefs.open(id)
+    },
+    async handleExecute (id) {
+      try {
+        const { data } = await executeDataTasks(id, {
+          execution_mode: 'complete',
+          backup_vector_tables: false, // 备份vector表数据
+          truncate_vector_tables: false // 清空vector表数据(自动启用备份)
+        })
+        this.$snackbar.success(data.response)
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            await deleteDataTasks({
+              task_ids: ids,
+              confirm: true
+            })
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 82 - 0
src/views/modelSystem/modelTrain/modelTrainEdit.vue

@@ -0,0 +1,82 @@
+<template>
+  <m-dialog title="创建任务" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  createDataTasks
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainEdit',
+  components: { MDialog, MForm },
+  data () {
+    return {
+      show: false,
+      formValues: {},
+      formItems: [
+        {
+          label: '任务名称',
+          key: 'task_name',
+          type: 'text',
+          rules: [v => !!v || '请输入任务名称']
+        },
+        {
+          label: 'PostgreSQL连接字符串',
+          key: 'db_connection',
+          type: 'text',
+          rules: [v => !!v || '请输入PostgreSQL连接字符串']
+        },
+        {
+          label: '数据库名称',
+          key: 'db_name',
+          type: 'text',
+          rules: [v => !!v || '请输入数据库名称']
+        },
+        {
+          label: '业务上下文描述',
+          key: 'business_context',
+          type: 'textarea',
+          rules: [v => !!v || '请输入业务上下文描述']
+        }
+      ],
+      loading: false
+    }
+  },
+  methods: {
+    open (item) {
+      this.show = true
+    },
+    async handleSubmit () {
+      const check = this.$refs.form.validate()
+      if (!check) {
+        return
+      }
+      this.loading = true
+      try {
+        await createDataTasks({
+          ...this.formValues,
+          enable_sql_validation: true,
+          enable_llm_repair: true,
+          modify_original_file: true,
+          enable_training_data_load: true
+        })
+        this.$snackbar.success('创建成功')
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 151 - 0
src/views/modelSystem/modelTrain/modelTrainFiles.vue

@@ -0,0 +1,151 @@
+<template>
+  <m-dialog title="任务文件" :visible.sync="show" :footer="false">
+    <m-table
+      class="mt-3"
+      elevation="0"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :disable-sort="true"
+      :is-tools="false"
+      :show-select="false"
+    >
+      <template #navBtn>
+        <v-btn class="buttons" rounded color="primary" @click="showUpload = true">文件上传</v-btn>
+      </template>
+      <template #actions="{ item }">
+        <v-btn text color="primary" :loading="item.loading" @click="handleDownload(item)">下载文件</v-btn>
+      </template>
+    </m-table>
+    <m-dialog title="文件上传" :visible.sync="showUpload" @submit="handleSubmitFiles">
+      <div style="width: 500px; margin: 0 auto; min-height: 300px;">
+        <div class="d-flex">
+          <div class="mr-5">上传文件</div>
+          <v-file-input
+            v-model="file"
+            label="点击上传"
+            outlined
+            hide-details
+            dense
+          ></v-file-input>
+        </div>
+        <div class="d-flex">
+          <div class="mt-5 mr-5">上传模式</div>
+          <v-radio-group
+            v-model="overwriteMode"
+          >
+            <v-radio
+              label="备份模式"
+              value="backup"
+            ></v-radio>
+            <v-radio
+              label="替换模式"
+              value="replace"
+            ></v-radio>
+            <v-radio
+              label="跳过模式"
+              value="skip"
+            ></v-radio>
+          </v-radio-group>
+        </div>
+      </div>
+    </m-dialog>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MTable from '@/components/List/table.vue'
+import {
+  getTasksFileList,
+  downloadTasksFileList,
+  uploadTasksList
+} from '@/api/dataChart'
+import { downloadFile } from '@/utils'
+export default {
+  name: 'modelTrainFiles',
+  components: {
+    MDialog,
+    MTable
+  },
+  data () {
+    return {
+      file: null,
+      overwriteMode: 'backup',
+      showUpload: false,
+      show: false,
+      id: null,
+      loading: false,
+      headers: [
+        { text: '文件名', value: 'file_name' },
+        { text: '文件类型', value: 'file_type' },
+        { text: '文件大小', value: 'file_size_formatted' },
+        { text: '操作', value: 'actions' }
+      ],
+      items: [],
+      total: 0,
+      pageInfo: {
+        current: 1,
+        size: 10
+      }
+    }
+  },
+  methods: {
+    open (id) {
+      this.id = id
+      this.show = true
+      this.getList()
+    },
+    async getList () {
+      this.loading = true
+      try {
+        const { data } = await getTasksFileList(this.id)
+        this.items = data.files.map(e => {
+          return {
+            ...e,
+            loading: false
+          }
+        })
+        this.total = data.directory_info.total_files
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async handleDownload (item) {
+      try {
+        item.loading = true
+        const { data, name } = await downloadTasksFileList(this.id, item.file_name)
+        downloadFile(data, name)
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        item.loading = false
+      }
+    },
+    async handleSubmitFiles () {
+      if (!this.file) {
+        this.$snackbar.error('请选择文件')
+        return
+      }
+      const query = new FormData()
+      query.append('file', this.file)
+      query.append('overwrite_mode', this.overwriteMode)
+      try {
+        await uploadTasksList(this.id, query)
+        this.$snackbar.success('上传成功')
+        this.showUpload = false
+        this.getList()
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 83 - 0
src/views/modelSystem/modelTrain/modelTrainLog.vue

@@ -0,0 +1,83 @@
+<template>
+  <v-navigation-drawer
+      v-model="drawer"
+      absolute
+      right
+      temporary
+      overlay-opacity="0"
+      width="700px"
+  >
+    <div class="pa-3 white sticky d-flex align-center justify-end">
+      <DatePicker
+        v-model="date"
+        ref="picker"
+        :option="{
+          range: true,
+          placeholder: '请选择时间',
+          type: 'date',
+          clearable: false
+        }"
+        @change="init"
+      ></DatePicker>
+      <!-- <v-btn rounded color="primary" class="ml-3 half-button">查 询</v-btn> -->
+    </div>
+    <v-timeline
+      :reverse="false"
+      dense
+    >
+      <v-timeline-item
+        v-for="(item, index) in items"
+        :key="index"
+        :color="item.level.toLowerCase()"
+      >
+      <div>{{ item.timestamp }} </div>
+      <div :class="{'red--text' : item.level === 'ERROR'}">{{ item.message }}</div>
+
+      </v-timeline-item>
+    </v-timeline>
+  </v-navigation-drawer>
+</template>
+
+<script>
+import DatePicker from '@/components/Form/datePicker.vue'
+import {
+  getTasksLog
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainLog',
+  components: {
+    DatePicker
+  },
+  data () {
+    return {
+      drawer: false,
+      date: null,
+      items: [],
+      id: null
+    }
+  },
+  methods: {
+    async open (id) {
+      this.id = id
+      this.drawer = true
+      this.init()
+    },
+    async init () {
+      try {
+        const { data } = await getTasksLog(this.id, {})
+        this.items = data.logs
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sticky {
+  position: sticky;
+  top: 0;
+  z-index: 3;
+}
+</style>

+ 184 - 0
src/views/modelSystem/modelTrain/modelTrainStatus.vue

@@ -0,0 +1,184 @@
+<template>
+  <m-dialog title="任务进度" :visible.sync="show" :footer="false">
+    <div style="min-height: 300px; " v-loading="loading">
+      <div class="text-right">
+        <v-btn color="primary" @click="getStatus">
+          <v-icon left>mdi-refresh</v-icon>
+          刷新
+        </v-btn>
+      </div>
+      <v-stepper
+        alt-labels
+        class="my-12"
+        elevation="0"
+      >
+        <v-stepper-header>
+          <template
+            v-for="(status, index) in statusItems"
+          >
+            <v-stepper-step
+              :key="status.key + index"
+              :step="index + 1"
+              :complete="status.value === 'completed'"
+              :rules="[() => status.value !== 'failed']"
+            >
+              {{ status.key }}
+              <div class="mt-3 text-center">
+                <v-chip :color="statusList[status.value]">{{ status.value }}</v-chip>
+              </div>
+              <div class="text-center mt-3">
+                <v-btn color="primary" :loading="status.loading" @click="handleRun(status)">
+                  <v-icon left>mdi-play</v-icon>
+                  重新执行
+                </v-btn>
+              </div>
+              <div class="text-center mt-3">
+                <v-btn :loading="status.loading" @click="handleTest(status)">
+                  <v-icon left>mdi-play</v-icon>
+                  测试执行
+                </v-btn>
+              </div>
+            </v-stepper-step>
+
+            <v-divider :key="status.key" v-if="index < statusItems.length - 1"></v-divider>
+          </template>
+        </v-stepper-header>
+      </v-stepper>
+    </div>
+    <m-dialog title="确认执行" :visible.sync="showConfirm" :widthType="2" @submit="handleSubmit">
+      <MForm :items="formItems" v-model="formValues" v-loading="loadingTest"></MForm>
+    </m-dialog>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import { getDataTasksStatus, executeDataTasks } from '@/api/dataChart'
+export default {
+  name: 'modelTrainStatus',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      showConfirm: false,
+      statusList: {
+        preparing: 'primary',
+        completed: 'success',
+        failed: 'error',
+        pending: 'warning'
+      },
+      show: false,
+      loading: false,
+      statusItems: [],
+      id: null,
+      loadingTest: false,
+      formItems: [
+        {
+          label: '备份vector表数据到CSV文件',
+          type: 'ifRadio',
+          key: 'backup_vector_tables',
+          width: 180,
+          items: [
+            { label: '是', value: true },
+            { label: '否', value: false }
+          ]
+        },
+        {
+          label: '清空vector表数据',
+          type: 'ifRadio',
+          key: 'truncate_vector_tables',
+          width: 180,
+          items: [
+            { label: '是', value: true },
+            { label: '否', value: false }
+          ]
+        },
+        {
+          label: '跳过训练文件处理',
+          type: 'ifRadio',
+          key: 'skip_training',
+          width: 180,
+          items: [
+            { label: '是', value: true },
+            { label: '否', value: false }
+          ]
+        }
+      ],
+      formValues: {
+        backup_vector_tables: false,
+        truncate_vector_tables: false,
+        skip_training: false
+      },
+      itemData: null
+    }
+  },
+  methods: {
+    async open (id) {
+      this.id = id
+      this.show = true
+      this.getStatus()
+    },
+    async getStatus () {
+      this.loading = true
+      try {
+        const { data } = await getDataTasksStatus(this.id)
+        this.statusItems = Object.keys(data.step_status).map(key => {
+          return {
+            key,
+            value: data.step_status[key],
+            loading: false
+          }
+        })
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleTest (item) {
+      this.itemData = item
+      this.formValues = {
+        backup_vector_tables: false,
+        truncate_vector_tables: false,
+        skip_training: false
+      }
+      this.showConfirm = true
+    },
+    async handleSubmit () {
+      this.loadingTest = true
+      const check = await this.handleRun(this.itemData, this.formValues)
+      this.loadingTest = false
+      if (check) {
+        this.showConfirm = false
+      }
+    },
+    async handleRun (item, data = {
+      backup_vector_tables: false,
+      truncate_vector_tables: false
+    }) {
+      item.loading = true
+      try {
+        await executeDataTasks(this.id, {
+          execution_mode: 'step',
+          step_name: item.key,
+          ...data
+        })
+        this.$snackbar.success('任务已启动')
+        this.getStatus()
+        return true
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        item.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 111 - 0
src/views/modelSystem/modelTrain/modelTrainTable.vue

@@ -0,0 +1,111 @@
+<template>
+  <m-dialog title="表名清单" :visible.sync="show" :footer="false">
+    <MTable
+      elevation="0"
+      :items="items"
+      :headers="headers"
+      :loading="loading"
+      :total="items.length"
+      :show-select="false"
+      :is-tools="false"
+    >
+      <template #navBtn>
+        <v-btn class="mr-3" color="primary" rounded @click="handleAdd">在线提交表名</v-btn>
+        <UploadBtn color="primary" accept=".txt" @change="handleChangeFile">上传表清单文件</UploadBtn>
+      </template>
+    </MTable>
+    <!-- <div
+      style="min-height: 300px;"
+      v-loading="loading"
+    >
+      <v-list
+        subheader
+      >
+        <v-list-item
+          v-for="item in items"
+          :key="item"
+        >
+          <v-list-item-content>
+            <v-list-item-title>{{ item }}</v-list-item-title>
+          </v-list-item-content>
+        </v-list-item>
+      </v-list>
+    </div> -->
+    <ModelTrainTableSubmit ref="modelTrainTableSubmitRefs" @success="getData"></ModelTrainTableSubmit>
+  </m-dialog>
+</template>
+
+<script>
+import MTable from '@/components/List/table'
+import UploadBtn from '@/components/UploadBtn'
+import MDialog from '@/components/Dialog'
+import ModelTrainTableSubmit from './modelTrainTableSubmit'
+import {
+  getDataBaseList,
+  uploadDataTasksList
+} from '@/api/dataChart'
+// import loading from '@/directives/loading'
+export default {
+  name: 'modelTrainTable',
+  components: {
+    MTable,
+    MDialog,
+    UploadBtn,
+    ModelTrainTableSubmit
+  },
+  data () {
+    return {
+      show: false,
+      loading: false,
+      items: [],
+      headers: [
+        { text: '表名', value: 'table_name' }
+      ],
+      id: null
+    }
+  },
+  methods: {
+    open (id) {
+      this.id = id
+      this.show = true
+      this.getData()
+    },
+    async getData () {
+      try {
+        this.loading = true
+        const { data } = await getDataBaseList({
+          task_id: this.id
+        })
+        this.items = data.tables.map(e => ({
+          table_name: e
+        }))
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleAdd () {
+      this.$refs.modelTrainTableSubmitRefs.open(this.id)
+    },
+    async handleChangeFile (file) {
+      const query = new FormData()
+      query.append('file', file)
+      this.loading = true
+      try {
+        await uploadDataTasksList(this.id, query)
+        this.$snackbar.success('更新成功')
+        this.getData()
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 63 - 0
src/views/modelSystem/modelTrain/modelTrainTableSubmit.vue

@@ -0,0 +1,63 @@
+<template>
+  <m-dialog title="修改表名" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MForm from '@/components/MForm'
+import MDialog from '@/components/Dialog'
+import {
+  saveDataBase
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainTableSubmit',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      show: false,
+      formItems: [
+        {
+          label: 'tables',
+          key: 'tables',
+          type: 'textarea',
+          placeholder: '请输入表名,表名使用逗号分隔,支持 schema.table 的格式',
+          rows: 8,
+          rules: [v => !!v || '不能为空']
+        }
+      ],
+      formValues: {},
+      id: null,
+      loading: false
+    }
+  },
+  methods: {
+    open (id) {
+      this.id = id
+      this.show = true
+    },
+    async handleSubmit () {
+      if (!this.$refs.form.validate()) return
+      this.loading = true
+      try {
+        await saveDataBase(this.id, this.formValues)
+        this.$snackbar.success('更新成功')
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 254 - 0
src/views/modelSystem/modelTrainManage/index.vue

@@ -0,0 +1,254 @@
+<template>
+  <div class="content white">
+    <v-banner class="mb-3">
+      训练数据统计
+      <template v-slot:actions>
+        <v-btn
+          text
+          color="primary"
+          @click="getStatistics"
+        >
+          <v-icon left>mdi-refresh</v-icon>
+          刷新
+        </v-btn>
+      </template>
+    </v-banner>
+    <v-container v-loading="loading">
+      <v-row >
+        <v-col
+          v-for="elevation in elevations"
+          :key="elevation.value"
+        >
+          <v-card
+            class="pa-2"
+            tile
+            height="150"
+          >
+            <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+              <div class="text-h3" :class="`${elevation.textColor}--text`">{{ itemData[elevation.value] }}</div>
+              <div>
+                {{ elevation.text }}
+                <span class="success--text">
+                  {{ itemData.type_percentages?.[elevation.value] ? `( ${itemData.type_percentages?.[elevation.value]}% )` : null }}
+                </span>
+              </div>
+            </div>
+          </v-card>
+        </v-col>
+      </v-row>
+
+    </v-container>
+    <v-divider class="my-3"></v-divider>
+    <div class="pa-3">
+      <MSearch class="mb-3" :option="filter" @search="handleSearch"></MSearch>
+      <MTable
+        :headers="headers"
+        :items="items"
+        :loading="tableLoading"
+        :total="total"
+        :page-info="pageInfo"
+        @delete="handleDelete"
+        @pageHandleChange="pageHandleChange"
+        @sort="handleSort"
+        @add="handleAdd"
+      >
+        <template #navBtn>
+          <v-btn color="primary" class="buttons mr-3" rounded elevation="5" @click="handleCombine">
+            <v-icon left>mdi-vector-combine</v-icon>
+            去重
+          </v-btn>
+          <v-btn color="primary" class="buttons" rounded elevation="5" @click="handleUpload">
+            <v-icon left>mdi-import</v-icon>
+            导入
+          </v-btn>
+        </template>
+        <template #question="{ item }">
+          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.question }}</span>
+        </template>
+        <template #content="{ item }">
+          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.content }}</span>
+        </template>
+        <template #actions="{ item }">
+          <v-btn text color="primary" @click="handleDetails(item)">查看</v-btn>
+          <v-btn text color="primary" @click="handleEdit(item)">编辑</v-btn>
+          <v-btn text color="error" @click="handleDelete([item.id])">删除</v-btn>
+        </template>
+      </MTable>
+    </div>
+    <ModelTrainManageEdit ref="ModelTrainManageEditRefs" @success="onOpen"></ModelTrainManageEdit>
+    <ModelTrainManageCombine ref="modelTrainManageCombineRefs" @success="init"></ModelTrainManageCombine>
+    <ModelTrainManageUpload ref="modelTrainManageUploadRefs" @success="init"></ModelTrainManageUpload>
+  </div>
+</template>
+
+<script>
+import {
+  getTrainingData,
+  getTrainingDataList,
+  deleteTrainingData
+} from '@/api/dataChart'
+import MTable from '@/components/List/table.vue'
+import MSearch from '@/components/Filter'
+import ModelTrainManageEdit from './modelTrainManageEdit.vue'
+import ModelTrainManageCombine from './modelTrainManageCombine.vue'
+import ModelTrainManageUpload from './modelTrainManageUpload.vue'
+export default {
+  name: 'ModelTrain',
+  components: {
+    MTable,
+    MSearch,
+    ModelTrainManageEdit,
+    ModelTrainManageUpload,
+    ModelTrainManageCombine
+  },
+  data () {
+    return {
+      searchValues: {},
+      filter: {
+        list: [
+          { type: 'textField', value: null, label: '关键词', key: 'search_keyword', placeholder: '请输入关键词' },
+          {
+            type: 'autocomplete',
+            key: 'training_data_type',
+            value: null,
+            label: '类型 *',
+            placeholder: '请选择类型',
+            items: [
+              { label: 'sql', value: 'sql' },
+              { label: 'documentation', value: 'documentation' },
+              { label: 'ddl', value: 'ddl' },
+              { label: 'error_sql', value: 'error_sql' }
+            ]
+          }
+        ]
+      },
+      loading: false,
+      elevations: [
+        { text: '总数据量', value: 'total_count', textColor: 'primary' },
+        { text: 'DDL类型', value: 'ddl', textColor: 'info' },
+        { text: 'SQL类型', value: 'sql', textColor: 'info' },
+        { text: '文档类型', value: 'documentation', textColor: 'info' },
+        { text: '错误SQL类型', value: 'error_sql', textColor: 'error' }
+      ],
+      itemData: {},
+      tableLoading: false,
+      headers: [
+        { text: 'ID', value: 'id' },
+        { text: '问题', value: 'question' },
+        { text: '类型', value: 'training_data_type' },
+        { text: '内容', value: 'content' },
+        { text: '操作', value: 'actions' }
+      ],
+      items: [],
+      total: 0,
+      orders: [],
+      pageInfo: {
+        size: 5,
+        current: 1
+      }
+    }
+  },
+  created () {
+    this.onOpen()
+  },
+  methods: {
+    onOpen () {
+      this.init()
+      this.getStatistics()
+    },
+    async getStatistics () {
+      this.loading = true
+      try {
+        const { data } = await getTrainingData()
+        this.itemData = {
+          ...data,
+          ...data.type_breakdown
+        }
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async init () {
+      this.tableLoading = true
+      const orders = {
+        sort_by: undefined,
+        sort_order: undefined
+      }
+      if (this.orders.length) {
+        Object.assign(orders, {
+          sort_by: this.orders[0].column.replace(/`/g, ''),
+          sort_order: this.orders[0].asc ? 'asc' : 'desc'
+        })
+      }
+      try {
+        const { data } = await getTrainingDataList({
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size,
+          ...this.searchValues,
+          ...orders
+        })
+        this.items = data.records
+        this.total = data.pagination.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.tableLoading = false
+      }
+    },
+    handleSearch (v) {
+      this.searchValues = v
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleAdd () {
+      this.$refs.ModelTrainManageEditRefs.open()
+    },
+    handleDetails (item) {
+      this.$refs.ModelTrainManageEditRefs.open(item, true)
+    },
+    handleCombine () {
+      this.$refs.modelTrainManageCombineRefs.open()
+    },
+    async handleUpload (file) {
+      this.$refs.modelTrainManageUploadRefs.open()
+    },
+    pageHandleChange (index) {
+      this.pageInfo.current = index
+      this.init()
+    },
+    handleSort (v) {
+      this.orders = v
+      this.init()
+    },
+    handleEdit (item) {
+      this.$refs.ModelTrainManageEditRefs.open(item)
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) {
+        this.$snackbar.warning('请选择要删除的数据')
+        return
+      }
+      this.$confirm('提示', '确定删除吗?').then(async () => {
+        try {
+          await deleteTrainingData({
+            ids,
+            confirm: true
+          })
+          this.$snackbar.success('删除成功')
+          this.onOpen()
+        } catch (error) {
+          this.$snackbar.error(error)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+  font-size: 16px;
+}
+</style>

+ 98 - 0
src/views/modelSystem/modelTrainManage/modelTrainManageCombine.vue

@@ -0,0 +1,98 @@
+<template>
+  <m-dialog title="合并训练数据" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  combineTrainData
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainCombine',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      formItems: [
+        {
+          label: '数据集合名称',
+          key: 'collections_names',
+          type: 'autocomplete',
+          rules: [v => v.length > 0 || '请输入数据集合名称'],
+          multiple: true,
+          noAttach: true,
+          items: [
+            { label: 'sql', value: 'sql' },
+            { label: 'ddl', value: 'ddl' },
+            { label: 'documentation', value: 'documentation' },
+            { label: 'error_sql', value: 'error_sql' }
+          ]
+        },
+        {
+          label: '保留策略',
+          key: 'keep_strategy',
+          type: 'autocomplete',
+          noAttach: true,
+          items: [
+            { label: '保留第一条记录', value: 'first' },
+            { label: '保留最后一条记录', value: 'last' },
+            { label: '保留最新记录', value: 'by_metadata_time' }
+          ]
+        },
+        {
+          label: '预览模式',
+          key: 'dry_run',
+          type: 'ifRadio',
+          items: [
+            { label: '是', value: true },
+            { label: '否', value: false }
+          ]
+        }
+      ],
+      formValues: {
+        collections_names: [],
+        keep_strategy: 'first',
+        dry_run: true
+      }
+    }
+  },
+  methods: {
+    open () {
+      this.show = true
+      this.formValues = {
+        collections_names: [],
+        keep_strategy: 'first',
+        dry_run: true
+      }
+      this.$nextTick(() => {
+        this.$refs.form.resetValidation()
+      })
+    },
+    async handleSubmit () {
+      if (!this.$refs.form.validate()) return
+      this.loading = true
+      try {
+        const { data } = await combineTrainData(this.formValues)
+        this.$snackbar.success(data.response)
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error.message)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 147 - 0
src/views/modelSystem/modelTrainManage/modelTrainManageEdit.vue

@@ -0,0 +1,147 @@
+<template>
+  <MDialog title="模型训练" width="600px" :visible.sync="visible" :footer="!readonly" @submit="onSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues"></MForm>
+  </MDialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  createTrainingData,
+  updateTrainingData
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainManage',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      visible: false,
+      readonly: false,
+      formValues: {
+        training_data_type: 'sql',
+        question: '',
+        content: ''
+      },
+      kinds: {
+        sql: {
+          text: 'SQL语句内容',
+          key: 'sql'
+        },
+        documentation: {
+          text: '文档内容',
+          key: 'content'
+        },
+        ddl: {
+          text: 'DDL语句内容', // ddl类型
+          key: 'ddl'
+        },
+        error_sql: {
+          text: 'SQL语句内容',
+          key: 'error_sql'
+        }
+      }
+    }
+  },
+  computed: {
+    formItems () {
+      const isSql = this.formValues.training_data_type === 'sql' || this.formValues.training_data_type === 'error_sql'
+      return [
+        {
+          label: '数据类型',
+          key: 'training_data_type',
+          type: 'ifRadio',
+          items: [
+            { label: 'sql', value: 'sql', readonly: this.readonly },
+            { label: 'documentation', value: 'documentation', readonly: this.readonly },
+            { label: 'ddl', value: 'ddl', readonly: this.readonly },
+            { label: 'error_sql', value: 'error_sql', readonly: this.readonly }
+          ],
+          rules: [v => this.readonly || !!v || '请选择数据类型'],
+          change: (val) => {
+            this.$nextTick(() => {
+              this.$refs.form.resetValidation()
+            })
+          }
+        },
+        {
+          label: `提问内容 ${isSql ? '*' : ''}`,
+          key: 'question',
+          readonly: this.readonly,
+          hide: this.formValues.training_data_type !== 'sql' && this.formValues.training_data_type !== 'error_sql',
+          type: 'textarea',
+          rules: [v => !isSql || this.readonly ? true : !!v || '请输入提问内容']
+        },
+        {
+          label: this.kinds[this.formValues.training_data_type].text,
+          key: 'content',
+          readonly: this.readonly,
+          rows: 10,
+          type: 'textarea',
+          rules: [v => this.readonly || !!v || '请输入回答内容']
+        }
+      ]
+    }
+  },
+  methods: {
+    open (item, readonly = false) {
+      this.readonly = readonly
+      this.visible = true
+      if (!item) {
+        this.formValues = {
+          training_data_type: 'sql'
+        }
+        this.$nextTick(() => {
+          this.$refs.form.resetValidation()
+        })
+        return
+      }
+      this.formValues = { ...item }
+      this.$nextTick(() => {
+        this.$refs.form.resetValidation()
+      })
+    },
+    async onSubmit () {
+      if (!this.$refs.form.validate()) return
+      // const a = {
+      //   data: {
+      //     training_data_type: '选择的类型',
+      //     question: '问题内容', // sql和error_sql类型必填
+      //     content: '文档内容', // documentation类型
+      //     ddl: 'DDL语句内容' // ddl类型
+      //     sql: 'SQL语句内容', // sql和error_sql类型
+      //   }
+      // }
+      const { content, question, training_data_type: type } = this.formValues
+      const query = {
+        id: this.formValues.id,
+        training_data_type: type,
+        [this.kinds[type].key]: content,
+        question: type === 'sql' || type === 'error_sql' ? question : undefined
+      }
+      try {
+        if (this.formValues.id) {
+          await updateTrainingData(query)
+        } else {
+          await createTrainingData({
+            data: query
+          })
+        }
+
+        this.$snackbar.success('创建成功')
+        this.visible = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 91 - 0
src/views/modelSystem/modelTrainManage/modelTrainManageUpload.vue

@@ -0,0 +1,91 @@
+<template>
+  <m-dialog title="导入训练数据" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  uploadTrainingData
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainManageUpload',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      show: false,
+      loading: false,
+      formValues: {},
+      formItems: [
+        {
+          label: '训练数据',
+          key: 'file',
+          type: 'upload',
+          multiple: true,
+          rules: [v => {
+            if (!v) {
+              return '请上传文件'
+            }
+            if (v.size > 500 * 1024) {
+              return '文件大小不能超过500KB'
+            }
+          }]
+        },
+        {
+          label: '文件类型',
+          key: 'file_type',
+          type: 'ifRadio',
+          items: [
+            { label: 'ddl', value: 'ddl' },
+            { label: 'markdown', value: 'markdown' },
+            { label: 'sql_pair_json', value: 'sql_pair_json' },
+            { label: 'sql_pair', value: 'sql_pair' },
+            { label: 'sql', value: 'sql' }
+          ]
+        }
+      ]
+    }
+  },
+  methods: {
+    open () {
+      this.show = true
+      this.formValues = {
+        file_type: 'ddl',
+        file: null
+      }
+      this.$nextTick(() => {
+        this.$refs.form.resetValidation()
+      })
+    },
+    async handleSubmit () {
+      const check = this.$refs.form.validate()
+      if (!check) {
+        return
+      }
+      this.loading = true
+      try {
+        const query = new FormData()
+        query.append('file', this.formValues.file)
+        query.append('training_data_type', this.formValues.training_data_type)
+        await uploadTrainingData()
+        this.$snackbar.success('导入成功')
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>