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

Merge branch 'jobFair' of https://git.citupro.com/zhengnaiwen_citu/menduner into jobFair

Xiao_123 3 недель назад
Родитель
Сommit
1433c4967a

+ 304 - 0
src/api/dataGovernance.js

@@ -0,0 +1,304 @@
+
+import request from '@/config/axios'
+
+// 元数据相关
+const metadata = {
+  // 新增元数据
+  async metadataAdd(data) {
+    return await request.post({ url: '/meta/node/add', data })
+  },
+  // 更新元数据
+  async metadataUpdate(data) {
+    return await request.post({ url: '/meta/node/update', data })
+  },
+  // 删除元数据
+  async deleteMetadata(data) {
+    return await request.post({ url: '/meta/node/delete', data })
+  },
+  // 查看元数据详情
+  getMetadataDetails: async (data) => {
+    return request.post({ url: '/meta/node/edit', data })
+  },
+  // 元数据列表
+  getMetaDataList: async (data) => {
+    return request.post({ url: '/meta/node/list', data })
+  },
+  // 元数据图谱
+  getMetaDataGraph: async (data) => {
+    return request.post({ url: '/meta/node/graph', data })
+  }
+}
+
+// 数据资源
+const dataResource = {
+  // 资源列表
+  async getResourceList(data) {
+    return await request.post({ url: '/resource/list', data })
+  },
+  // 资源详情
+  getResourceDetails: async (data) => {
+    return request.post({ url: '/resource/detail', data })
+  },
+  // 查看资源图谱   没接
+  getResourceListToGraph: async (data) => {
+    return request.post({ url: '/resource/list/graph', data })
+  },
+  // 删除资源
+  deleteSource: async (data) => {
+    return request.post({ url: '/resource/delete', data })
+  },
+  // 大模型模型翻译元数据  上传接口参数不对
+  resourceTranslate: async (data) => {
+    return request.formData({ url: '/resource/translate', data })
+  },
+  // 保存数据资源
+  saveResource: async (data) => {
+    return request.post({ url: '/resource/save', data })
+  },
+  // 数据资源上传  未知
+  uploadResource: async (data) => {
+    return request.upload({ url: '/meta/resource/upload', data })
+  },
+  // 获取数据资源文件流  未知
+  getResourceFile: async (data) => {
+    return request.getDownload({ url: '/meta/resource/download', data })
+  },
+  // 解析非结构化文本展示信息  未知
+  getUnstructured: async (data) => {
+    return request.post({ url: '/text/resource/translate', data })
+  },
+  // 查看资源图谱
+  getResourceGraph: async (data) => {
+    return request.post({ url: '/resource/graph/all', data })
+  },
+  // 解析一个文件中多份DDL 文件上传接口 参数错误 预留接口
+  resourceParseByDDL: async (data) => {
+    return request.upload({ url: '/resource/ddl/identify', data })
+  },
+  resourceParseDDL: async (data) => {
+    return request.upload({ url: '/resource/ddl/parse', data })
+  },
+  // DDL数据资源更新 未确认
+  resourceUpdateByDDL: async (data) => {
+    return request.post({ url: '/resource/update', data })
+  },
+  // 通过资源id查找元数据
+  getMetaDataById: async (data) => {
+    return request.post({ url: '/resource/search', data })
+    // return request.post({ url: '/id/data/search', data })
+  },
+  // 数据资源生产线调度 结构化 手动执行
+  productionLineDispatch: async (data) => {
+    return request.post({ url: '/pipeline/production/line/execute', data })
+  }
+}
+
+// 数据模型
+const dataModel = {
+  // 模型列表
+  getModelList: async (data) => {
+    return request.post({ url: '/model/data/model/list', data })
+  },
+  // 查看模型详情
+  getModelDetails: async (data) => {
+    return request.post({ url: '/model/data/model/detail', data })
+  },
+  // 查看模型图谱
+  getModelListToGraph: async (data) => {
+    return request.post({ url: '/model/data/model/graph/all', data })
+  },
+  // 新增模型 -> 通过资源新增模型
+  addModel: async (data) => {
+    // return request.post({ url: '/model/model/data/model/add', data })
+    return request.post({ url: '/model/data/search', data })
+  },
+  // 删除模型
+  deleteModel: async (data) => {
+    return request.post({ url: '/model/data/model/delete', data })
+  },
+  // 新增模型:模型选择模型 未知
+  addModelByModel: async (data) => {
+    return request.post({ url: '/model/data/model/add', data })
+  },
+  // 查看模型图谱
+  getModelGraph: async (data) => {
+    return request.post({ url: '/model/graph/all', data })
+  },
+  // 更新模型
+  updateModel: async (data) => {
+    return request.post({ url: '/model/data/model/update', data })
+  },
+  // 通过ddl保存模型
+  saveModelByDDL: async (data) => {
+    return request.post({ url: '/model/data/model/save', data })
+  }
+}
+
+// 数据标签
+const dataLabel = {
+  // 列表
+  dataLabelList: async (data) => {
+    return request.post({ url: '/interface/data/label/list', data })
+  },
+  // 新增
+  dataLabelAdd: async (data) => {
+    return request.post({ url: '/interface/data/label/add', data })
+  },
+  // 详情
+  dataLabelDetails: async (data) => {
+    return request.post({ url: '/interface/data/label/detail', data })
+  },
+  // 标签图谱
+  dataLabelGraph: async (data) => {
+    return request.post({ url: '/interface/data/label/graph/all', data })
+  },
+  // 数据标签动态识别分组
+  dataLabelIdentifyGroup: async (data) => {
+    return request.post({ url: '/interface/data/label/dynamic/identify', data })
+  }
+}
+
+// 数据标准
+const dataStandard = {
+  // 标准列表
+  dataStandardList: async (data) => {
+    return request.post({ url: '/interface/data/standard/list', data })
+  },
+  // 新增
+  dataStandardAdd: async (data) => {
+    return request.post({ url: '/interface/data/standard/add', data })
+  },
+  // 详情
+  dataStandardDetails: async (data) => {
+    return request.post({ url: '/interface/data/standard/detail', data })
+  },
+  // 生成操作代码
+  dataStandardCodeGenerate: async (data) => {
+    return request.post({ url: '/interface/data/standard/code', data })
+  },
+  // 标签图谱
+  dataStandardGraph: async (data) => {
+    return request.post({ url: '/interface/data/standard/graph/all', data })
+  }
+}
+
+// 数据指标
+const dataIndicator = {
+  // 指标列表
+  dataIndicatorList: async (data) => {
+    return request.post({ url: '/metric/data/metric/list', data })
+  },
+  // 指标列表切换图谱
+  dataIndicatorListToGraph: async (data) => {
+    return request.post({ url: '/metric/data/metric/list/graph', data })
+  },
+  // 新增指标
+  dataIndicatorAdd: async (data) => {
+    return request.post({ url: '/metric/data/metric/add', data })
+  },
+  // 新增更新
+  dataIndicatorUpdate: async (data) => {
+    return request.post({ url: '/metric/data/metric/update', data })
+  },
+  // 指标详情
+  dataIndicatorDetails: async (data) => {
+    return request.post({ url: '/metric/data/metric/detail', data })
+  },
+  // 指标血缘关系检测
+  dataIndicatorRelation: async (data) => {
+    return request.post({ url: '/metric/data/metric/relation', data })
+  },
+  // 指标生成代码
+  dataIndicatorCodeGenerate: async (data) => {
+    return request.post({ url: '/metric/data/metric/code', data })
+  },
+  // 指标图谱
+  dataIndicatorGraph: async (data) => {
+    return request.post({ url: '/metric/graph/all', data })
+  }
+}
+
+const LLM = {
+
+  // 模型训练
+  setDDLTrain: async (data) => {
+    return request.post({ url: '/vanna/api/v0/train', data })
+  },
+  // 图表sql信息
+  getSql: async (params) => {
+    return request.get({ url: '/vanna/api/v0/generate_sql', params})
+  },
+  // 图表sql信息
+  getToTable: async (params) => {
+    return request.get({ url: '/vanna/api/v0/run_sql', params})
+  },
+  // 知识库+图谱问答
+  ask: async (data) => {
+    return request.post({ url: '/rag/ask', data })
+  },
+  // 产品知识库问答 RAG
+  askToProduct: async (data) => {
+    return request.post({ url: '/rag/ask/rag', data })
+  },
+  // 非结构化知识库问答 RAG + Graph
+  askToUnstructured: async (data) => {
+    return request.post({ url: '/rag/ask/unstructure/rag', data })
+  }
+}
+
+const other = {
+// 资源 & 模型 列表 未接
+  getResourceAndModelList: async (data) => {
+    return request.post({ url: '/resource/model/list', data })
+  },
+  // 获取资源ddl 未接
+  getDDL: async (data) => {
+    return request.post({ url: '/id/data/ddl', data })
+  },
+
+  // 生产线执行非结构化文档 实体 关系
+  runUnstructured: async (data) => {
+    return request.post({ url: '/text/resource/node', data })
+  },
+  // 生产线执行非结构化文档 元数据内容
+  runUnstructuredMetadata: async (data) => {
+    return request.post({ url: '/processing/unstructured/data', data })
+  },
+  // 产品知识库列表
+  productKnowledgeBaseList: async (data) => {
+    return request.post({ url: '/text/product/list', data })
+  },
+  // 直接删除图谱元素
+  graphDataDelete: async (data) => {
+    return request.post({ url: '/metric/label/standard/delete', data })
+  },
+  // 直接删除图谱元素
+  getGraphMetadataById: async (data) => {
+    return request.post({ url: '/graph/meta/include', data })
+  }
+}
+
+// 整合api
+export const api = {
+  // 元数据
+  ...metadata,
+  // 数据资源
+  ...dataResource,
+  // 数据模型
+  ...dataModel,
+
+  // 数据标签
+  ...dataLabel,
+
+  // 数据标准
+  ...dataStandard,
+
+  // 数据指标
+  ...dataIndicator,
+
+  // 大语言模型对话接口
+  ...LLM,
+
+  ...other
+
+}

+ 139 - 0
src/components/Knowledge/index.vue

@@ -0,0 +1,139 @@
+<template>
+  <div>
+    <v-avatar
+      ref="box"
+      class="box point elevation-5"
+      color="indigo"
+      @mousedown="handleMousedown"
+      @click="handleClick"
+    >
+      <v-icon dark large>
+        mdi-face-agent
+      </v-icon>
+    </v-avatar>
+    <v-navigation-drawer
+      v-model="show"
+      absolute
+      temporary
+      hide-overlay
+      right
+      width="500"
+      style="z-index: 400"
+    >
+      <m-talk v-if="show" :title="title" :type="type" v-bind="$attrs">
+        <template v-for="name in Object.keys(this.$scopedSlots)" v-slot:[`${name}`]="slotProps">
+          <slot :name="name" v-bind="slotProps"></slot>
+        </template>
+      </m-talk>
+    </v-navigation-drawer>
+  </div>
+</template>
+
+<script>
+import MTalk from './talk'
+export default {
+  name: 'knowledge-component',
+  components: {
+    MTalk
+  },
+  props: {
+    // 1 手册探索 2 图表实验室 3 产品知识库
+    type: {
+      type: Number,
+      default: 1
+    },
+    title: {
+      type: String,
+      default: '数据探索'
+    },
+    handle: Function
+  },
+  data () {
+    return {
+      show: false,
+      lock: false,
+      isDragging: false,
+      info: {}
+    }
+  },
+  mounted () {
+    this.$nextTick(() => {
+      document.addEventListener('mouseup', this.handleMouseup)
+      document.addEventListener('mousemove', this.handleMousemove)
+      this.$once('hook:beforeDestroy', () => {
+        document.removeEventListener('mouseup', this.handleMouseup)
+        document.removeEventListener('mousemove', this.handleMousemove)
+      })
+    })
+  },
+  methods: {
+    handleClick () {
+      if (this.isDragging) {
+        return
+      }
+      if (this.handle) {
+        this.handle()
+        return
+      }
+      this.show = !this.show
+    },
+    // 记录当前位置
+    handleMousedown (e) {
+      this.lock = true
+      const { left, top, height, width } = this.$refs.box.$el.getBoundingClientRect()
+      this.info = {
+        click: {
+          x: e.clientX,
+          y: e.clientY
+        },
+        left,
+        top,
+        height,
+        width,
+        clientX: e.clientX,
+        clientY: e.clientY
+      }
+    },
+    // 移动位置
+    handleMousemove (e) {
+      if (!this.lock) return
+      const xLen = Math.abs(this.info.click?.x - e.clientX)
+      const yLen = Math.abs(this.info.click?.y - e.clientY)
+      // 拖动距离触发
+      if (xLen < 20 && yLen < 20) {
+        return
+      }
+
+      // 计算移动距离
+      const clientX = e.clientX - this.info.clientX
+      const clientY = e.clientY - this.info.clientY
+      this.isDragging = true
+      this.info.left += clientX
+      this.info.top += clientY
+      this.$refs.box.$el.style.left = this.info.left + 'px'
+      this.$refs.box.$el.style.top = this.info.top + 'px'
+      this.info.clientX = e.clientX
+      this.info.clientY = e.clientY
+    },
+    handleMouseup (e) {
+      this.lock = false
+      setTimeout(() => {
+        this.isDragging = false
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.point {
+  cursor: pointer;
+
+}
+.box {
+  z-index: 9;
+  position: fixed;
+  right: 50px;
+  bottom: 100px;
+}
+</style>

+ 346 - 0
src/components/Knowledge/talk.vue

@@ -0,0 +1,346 @@
+<template>
+  <div class="d-flex flex-column" style="height: 100%">
+    <v-banner single-line>
+      <div class="my-3">
+        {{ title }}
+      </div>
+      <template #actions>
+        <slot name="title"></slot>
+      </template>
+    </v-banner>
+    <div style="flex: 1; overflow: auto;" class="pa-3" ref="box">
+      <div style="height: 100%;">
+        <v-list three-line>
+          <!-- 欢迎语 -->
+          <v-list-item>
+            <v-list-item-content>
+              <v-list-item-title class="d-flex align-center" :class="system.nameColor + '--text'">
+                <v-icon class="mr-3" :color="system.iconColor" >
+                {{ system.icon }}
+                </v-icon>
+                {{ system.name }}
+              </v-list-item-title>
+              <v-list-item-subtitle class="d-flex">
+                <div class="lighten-5 pa-3 round" :class="system.bgColor">
+                  {{ system.welcome }}
+                </div>
+              </v-list-item-subtitle>
+            </v-list-item-content>
+          </v-list-item>
+          <v-list-item v-for="talk in talks" :key="talk.id">
+            <v-list-item-content>
+              <v-list-item-title
+                class="d-flex align-center"
+                :class="
+                `${talk.type === 'question' ? 'justify-end ' + user.nameColor + '--text' : system.nameColor + '--text'}`
+                "
+              >
+                {{ talk.type === 'question' ? $store.getters.userInfo.username : '' }}
+                <template v-if="talk.type === 'question'">
+                  <v-chip v-for="tag in talk.tags" :key="tag" x-small class="ml-2">{{ tag }}</v-chip>
+                </template>
+                <v-icon
+                  :class="talk.type === 'question' ? 'ml-3' : 'mr-3'"
+                  :color="talk.type === 'question' ? user.iconColor : system.iconColor"
+                >
+                  {{ talk.type === 'question' ? user.icon : system.icon }}
+                </v-icon>
+                {{ talk.type === 'question' ? '' : system.name }}
+                <template v-if="talk.type !== 'question'">
+                  <v-chip v-for="tag in talk.tags" :key="tag" x-small class="ml-2">{{ tag }}</v-chip>
+                </template>
+              </v-list-item-title>
+              <v-list-item-subtitle class="d-flex" :class="{'justify-end': talk.type === 'question'}">
+                <div class="lighten-5 pa-3 round" :class=" talk.type === 'question' ? user.bgColor : system.bgColor">
+                  <template v-if="talk.view === 'table'">
+                    <v-simple-table fixed-header dense height="350">
+                      <template v-slot:default>
+                        <thead>
+                          <tr>
+                            <th v-for="header in talk.content.headers" :key="header" class="text-left">
+                              {{header}}
+                            </th>
+                          </tr>
+                        </thead>
+                        <tbody>
+                          <tr
+                            v-for="(body, i) in talk.content.body"
+                            :key="i"
+                          >
+                            <td v-for="header in talk.content.headers" :key="header">{{ body[header] }}</td>
+                          </tr>
+                        </tbody>
+                      </template>
+                    </v-simple-table>
+                  </template>
+                  <template v-else>
+                    <div v-for="(content, index) in talk.content" :key="index + content">{{ content }}</div>
+                    <!-- {{ talk.content }} -->
+                  </template>
+                </div>
+              </v-list-item-subtitle>
+            </v-list-item-content>
+          </v-list-item>
+          <!-- loading -->
+          <v-list-item style="min-height: 10px" v-show="loading">
+            <v-list-item-content>
+              <v-list-item-title class="d-flex align-center" :class="system.nameColor + '--text'">
+                <v-icon class="mr-3" :color="system.iconColor">
+                  {{ system.icon }}
+                </v-icon>
+                <div class="mr-3">{{ system.name }} 搜索中 </div>
+                <v-progress-circular
+                  size="18"
+                  width="2"
+                  indeterminate
+                  :color="system.iconColor"
+                ></v-progress-circular>
+              </v-list-item-title>
+            </v-list-item-content>
+          </v-list-item>
+        </v-list>
+      </div>
+    </div>
+    <div class="send d-flex align-center justify-center">
+      <div class="send-box">
+        <v-textarea
+          v-model="question"
+          dense
+          class="send-box-area"
+          auto-grow
+          label="请输入您想问的内容,按 Ctrl+Enter 换行"
+          placeholder="请输入您想问的内容,按 Ctrl+Enter 换行"
+          solo
+          hide-details
+          no-resize
+          rows="1"
+          @keydown.enter="handleKeyCode($event)"
+        >
+        </v-textarea>
+        <v-btn icon color="primary" class="btn" :disabled="!question" @click="handleSendMsg">
+          <v-icon>mdi-send</v-icon>
+        </v-btn>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { api } from '@/api/dataGovernance'
+export default {
+  name: 'knowledge-talk',
+  props: {
+    // 1 手册探索 2 图表实验室 3 产品知识库 (RAG + Graph) 4 产品知识库 (RAG)
+    type: {
+      type: Number,
+      default: 1
+    },
+    title: {
+      type: String,
+      default: '数据探索'
+    },
+    welcome: {
+      type: String,
+      default: '您好,我是您的智能助手,有什么问题可以问我哦!'
+    }
+  },
+  data () {
+    return {
+      // 系统端信息配置
+      system: {
+        name: '智能助手',
+        nameColor: 'indigo',
+        icon: 'mdi-face-agent',
+        iconColor: 'indigo',
+        bgColor: 'indigo',
+        welcome: this.welcome
+      },
+      // 用户端信息配置
+      user: {
+        name: this.$store.getters.userInfo.username,
+        nameColor: 'blue',
+        icon: 'mdi-account-circle',
+        iconColor: 'blue',
+        bgColor: 'blue'
+      },
+      loading: false,
+      talks: [],
+      question: '',
+      list: [],
+      id: 0
+    }
+  },
+  watch: {
+    // 长度变化自动滑动至底部
+    talks: {
+      handler () {
+        this.$nextTick(() => {
+          this.$refs.box.scrollTo({
+            top: this.$refs.box.scrollHeight,
+            behavior: 'smooth'
+          })
+        })
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  methods: {
+    handleSendMsg () {
+      if (!this.question) {
+        return
+      }
+      switch (this.type) {
+        case 2:
+          this.getSql()
+          break
+        case 4:
+          this.talkTo(3, true)
+          break
+        default:
+          this.talkTo(this.type)
+          break
+      }
+    },
+    handleKeyCode (event) {
+      if (event.keyCode === 13) {
+        if (!event.ctrlKey) {
+          event.preventDefault()
+          this.handleSendMsg()
+        } else {
+          this.question += '\n'
+        }
+      }
+    },
+    async getSql () {
+      this.loading = true
+      this.talks.push({
+        id: this.id++,
+        type: 'question',
+        tags: [],
+        content: this.question.split('\n')
+      })
+      const params = {
+        question: this.question
+      }
+      this.question = ''
+      try {
+        const { data } = await api.getSql(params)
+        this.talks.push({
+          id: this.id++,
+          type: 'response',
+          tags: [],
+          content: data.text.split('\n')
+        })
+        this.getTable(data.id)
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async getTable (id) {
+      this.loading = true
+      const params = { id }
+      try {
+        const { data } = await api.getToTable(params)
+        if (data.type === 'error') {
+          return
+        }
+        const _table = JSON.parse(data.df)
+        if (!_table.length) {
+          return
+        }
+        // console.log(_table)
+        const _headers = Object.keys(_table[0])
+        this.talks.push({
+          id: this.id++,
+          type: 'response',
+          tags: [],
+          content: {
+            headers: _headers,
+            body: _table
+          },
+          view: 'table'
+        })
+        this.$emit('change', _headers, _table)
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async talkTo (useType, rag) {
+      this.talks.push({
+        id: this.id++,
+        type: 'question',
+        tags: [],
+        content: this.question.split('\n')
+      })
+      const param = {
+        question: this.question,
+        chat_history: this.list
+      }
+      this.question = ''
+      this.loading = true
+      const askApi = useType === 3 ? api.askToUnstructured : api.ask
+      try {
+        const { data } = await askApi(param)
+        this.talks.push({
+          id: this.id++,
+          type: 'response',
+          tags: useType === 3 ? ['RAG + Graph'] : [],
+          content: data.response.split('\n')
+        })
+        // 多获取一个图谱+RAG
+        if (rag) {
+          const { data: _data } = await api.askToProduct(param)
+          this.talks.push({
+            id: this.id++,
+            type: 'response',
+            tags: ['RAG'],
+            content: _data.response.split('\n')
+          })
+        }
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.round {
+  border-radius: 5px;
+  max-width: 80%;
+}
+.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;
+      }
+    }
+  }
+}
+</style>

+ 2 - 1
src/components/Position/similarPositions.vue

@@ -8,7 +8,7 @@
       <div :class="['enterprise', {'border-bottom-dashed': index !== list.length - 1}]">
         <v-img class="float-left entLogoImg" :src="item.logoUrl || 'https://minio.citupro.com/dev/menduner/company-avatar.png'" :width="30" :height="30"></v-img>
         <span class="float-left enterprise-name" v-ellipse-tooltip>{{ formatName(item.anotherName || item.enterpriseName) }}</span>
-        <span class="float-right enterprise-address">{{ !item.areaId ? '全国' : item.area?.str }}</span>
+        <span class="float-right enterprise-address" v-ellipse-tooltip>{{ !item.areaId ? '全国' : item.area?.str }}</span>
       </div>
     </div>
   </div>
@@ -75,5 +75,6 @@ const handlePosition = (item) => {
 .enterprise-address {
   color: #555;
   font-size: 13px;
+  max-width: 90px;
 }
 </style>