zhengnaiwen_citu 3 тижнів тому
батько
коміт
60de48e88a

+ 13 - 0
package-lock.json

@@ -3095,6 +3095,11 @@
       "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
       "dev": true
     },
+    "batch-processor": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/batch-processor/-/batch-processor-1.0.0.tgz",
+      "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA=="
+    },
     "big-integer": {
       "version": "1.6.52",
       "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz",
@@ -4717,6 +4722,14 @@
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.99.tgz",
       "integrity": "sha512-77c/+fCyL2U+aOyqfIFi89wYLBeSTCs55xCZL0oFH0KjqsvSvyh6AdQ+UIl1vgpnQQE6g+/KK8hOIupH6VwPtg=="
     },
+    "element-resize-detector": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmmirror.com/element-resize-detector/-/element-resize-detector-1.2.4.tgz",
+      "integrity": "sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==",
+      "requires": {
+        "batch-processor": "1.0.0"
+      }
+    },
     "emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",

+ 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",

+ 11 - 1
src/api/dataChart.js

@@ -3,5 +3,15 @@ import http from '@/utils/request'
 
 // vanna 问答
 export function getAsk (data) {
-  return http.post('/v0/ask', data)
+  return http.post('/vanna/v0/ask', data)
+}
+
+// 提交到正确训练集
+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)
 }

+ 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.getEl().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)
     }
   },

+ 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

+ 2 - 2
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,7 +62,7 @@ service.interceptors.response.use(
     return res
   },
   error => {
-    return Promise.reject(error)
+    return Promise.reject(error.message || error)
   }
 )
 

+ 65 - 33
src/views/dataChart/dataChartEdit.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="d-flex chart heightFull widthFull">
     <div 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(chart, key)">
+      <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>
@@ -12,63 +12,95 @@
     </div>
 
     <div class="chart-content d-flex heightFull">
-      <div class="chart-content-show heightFull mr-3" ref="box">
-        <LowCode ref="chart" :option="option" :height="height"></LowCode>
+      <div class="chart-content-show heightFull mr-3 overflow-hidden" ref="box">
+        <MEmpty v-if="empty"></MEmpty>
+        <InitChart ref="chart" class="heightFull widthFull"></InitChart>
       </div>
-      <DataChartEditChat></DataChartEditChat>
+      <DataChartEditChat @render="onRender"></DataChartEditChat>
     </div>
   </div>
 </template>
 
 <script>
+import MEmpty from '@/components/Common/empty.vue'
 // 属性模块
 import * as Charts from './utils/options.js'
 import DataChartEditChat from './dataChartEditChat.vue'
-import LowCode from '@/charts/lowCode'
+import InitChart from '@/charts/initChart'
+import { cloneDeep } from 'lodash'
 export default {
   name: 'dataChartEdit',
   components: {
     DataChartEditChat,
-    LowCode
+    InitChart,
+    MEmpty
   },
   data () {
     return {
       Charts,
-      height: 200,
-      option: {
-        xAxis: {
-          type: 'category',
-          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+      chart: null,
+      chartsOpt: {
+        data: [[]],
+        config: {
+          xAxisData: []
         },
-        yAxis: {
-          type: 'value'
-        },
-        series: [
-          {
-            data: [150, 230, 224, 218, 135, 147, 260],
-            type: 'line'
-          }
-        ]
-      },
-      chart: null
+        key: null
+      }
     }
   },
-  mounted () {
-    this.chart = this.$refs.chart.getChart()
-    this.onResize()
-    window.addEventListener('resize', this.onResize)
+  computed: {
+    empty () {
+      return !this.chartsOpt.config.xAxisData.length
+    }
   },
-  beforeDestroy () {
-    window.removeEventListener('resize', this.onResize)
+  mounted () {
+    this.chart = this.$refs.chart.init()
   },
   methods: {
-    onResize () {
-      this.$nextTick(() => {
-        this.height = this.$refs.box.clientHeight
-        setTimeout(() => {
-          this.chart.resize()
+    onChange (key) {
+      this.chartsOpt.key = key
+      this.setData()
+    },
+    onRender ({ type, data }) {
+      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)
+      }
+      console.log(_option)
+      console.log(this.chart)
+      this.chart.setOption(_option, true)
+      this.chart.hideLoading()
     }
   }
 }

+ 221 - 41
src/views/dataChart/dataChartEditChat.vue

@@ -5,7 +5,7 @@
         <v-tab>AI 取数</v-tab>
       </v-tabs>
     </div>
-    <div class="chart-content-chat-box overflow-y-auto" ref="chatBox">
+    <div class="chart-content-chat-box overflow-y-auto position-relative" ref="chatBox">
       <div class="pa-3">
         <div
           v-for="(item, index) in items"
@@ -15,8 +15,24 @@
           <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' : 'mr-3 box-length-70']">
-            <div :class="`text-${item.type === 1 ? 'left' : 'right'}`">{{ item.type === 1 ? 'AI助手' : '游客' }}</div>
+          <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 !== 1 }">
               <template v-if="typeof item.content === 'string'">
                 {{ item.content }}
@@ -35,27 +51,115 @@
               </template>
               <template v-else>
                 <div>
+                  {{ item.content.summary }}
+                </div>
+                <div v-if="item.showSnackbar" class="pa-3 blue-grey lighten-3">
                   {{ item.content.sql }}
                 </div>
-                <div class="mt-3">
-                  <!-- <m-table
-                    clearHeader
-                    size="small"
-                    shadow="never"
-                    :headers="item.content.columns"
-                    :items="item.content.rows"
-                  ></m-table>
-                  <el-popover
-                    placement="bottom"
-                    width="200"
-                    trigger="click"
-                  >
-                    <m-form :ref="`form${i}`" label-width="60px" :items="formItems(item.content.columns)" v-model="item.model"></m-form>
-                    <div style="text-align: right; margin: 0">
-                      <m-button type="primary" size="mini" @click="onRender($refs[`form${i}`], item)">生成图表</m-button>
+                <div class="mt-3" v-if="item.content.columns.length">
+                  <div>
+                    <v-menu
+                      :close-on-content-click="false"
+                      max-width="300"
+                      attach=".chart-content-chat-box"
+                    >
+                      <template v-slot:activator="{ on, attrs }">
+                        <v-btn
+                          v-bind="attrs"
+                          v-on="on"
+                          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.columns"
+                            class="mb-3"
+                            outlined
+                            dense
+                            hide-details
+                            label="类型轴"
+                          ></v-autocomplete>
+
+                          <v-autocomplete
+                            v-model="item.model.dataAxis"
+                            :items="item.content.columns"
+                            class="mb-3"
+                            outlined
+                            dense
+                            hide-details
+                            label="数据轴"
+                            multiple
+                            chips
+                            small-chips
+                          ></v-autocomplete>
+                          <div class="text-right">
+                            <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.columns"
+                                :key="header"
+                                class="text-left"
+                              >{{ header }}</th>
+                            </tr>
+                          </thead>
+                          <tbody>
+                            <tr
+                              v-for="(row, index) in item.content.rows"
+                              :key="index"
+                            >
+                              <td
+                                v-for="header in item.content.columns"
+                                :key="header"
+                                class="text-left"
+                              >{{ row[header] }}</td>
+                            </tr>
+                          </tbody>
+                        </template>
+                      </v-simple-table>
                     </div>
-                    <m-button type="primary" text slot="reference">我要作图</m-button>
-                  </el-popover> -->
+                  </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>
@@ -78,35 +182,70 @@
             @keydown.enter="handleKeyCode($event)"
           >
           </v-textarea>
-          <v-btn icon color="primary" class="btn" :disabled="!question" @click="handleSendMsg">
+          <v-btn icon color="primary" class="btn" :disabled="!question || disabled" @click="handleSendMsg">
             <v-icon>mdi-send</v-icon>
           </v-btn>
         </div>
       </div>
     </div>
+    <!-- <v-dialog
+      v-model="show"
+      persistent
+      max-width="290"
+    >
+      <v-card>
+        <v-card-title class="text-h5">
+          提示
+        </v-card-title>
+        <v-card-text>是否添加到训练集</v-card-text>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn
+            text
+            @click="show = false"
+          >
+            取消
+          </v-btn>
+          <v-btn
+            color="error darken-1"
+            text
+            @click="onSure(false)"
+          >
+            不添加
+          </v-btn>
+          <v-btn
+            color="green darken-1"
+            text
+            @click="onSure(true)"
+          >
+            添加
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog> -->
   </div>
 </template>
 
 <script>
 import {
-  getAsk
+  getAsk,
+  submitTrainingCorrect,
+  submitTrainingError
 } from '@/api/dataChart'
 export default {
   name: 'dataChartEditChat',
   data () {
     return {
-      icons: 1,
+      // show: false,
+      disabled: false,
       question: '',
       items: [
         {
           type: 1,
           content: '您好,我是AI助手,请问有什么可以帮助您的吗?'
-        },
-        {
-          type: 2,
-          content: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sed deserunt explicabo corrupti iure nesciunt autem quaerat unde, fugit, ipsa magni consequatur beatae vero ea culpa nisi aliquid aliquam consectetur aut.'
         }
       ]
+      // trueData: false
     }
   },
   methods: {
@@ -121,40 +260,75 @@ export default {
       }
     },
     async handleSendMsg () {
-      if (!this.question) {
+      if (!this.question || this.disabled) {
         return
       }
+      this.disabled = true
+
+      const question = this.question
       this.items.push({
         type: 2,
         user: '游客',
-        content: this.question
+        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 {
-        const { data } = await getAsk({
-          question: this.question
-        })
-        this.question = ''
-        const { columns, ...obj } = data
-        ask.content = {
-          ...obj,
-          columns: columns.map(e => ({ label: e, prop: e, value: e }))
+        const { data } = await getAsk({ question })
+        ask.content = data
+        this.scrollToBottom()
+      } catch (error) {
+        ask.content = error.message
+      } finally {
+        this.disabled = false
+      }
+    },
+    scrollToBottom () {
+      this.$nextTick(() => {
+        const box = this.$refs.chatBox
+        if (!box) {
+          return
         }
-        this.$nextTick(() => {
-          const box = this.$refs.chatBox
-          box.scrollTop = box.scrollHeight
+        box.scrollTop = box.scrollHeight
+      })
+    },
+    async onAddTrain (item, bool) {
+      const subApi = bool ? submitTrainingCorrect : submitTrainingError
+      try {
+        await subApi({
+          question: item.question,
+          sql: item.content.sql
         })
+        item.dataValidation = true
+        // this.$snackbar.success('操作成功')
       } catch (error) {
         this.$snackbar.error(error)
       }
+    },
+    onRender ({ model, content }) {
+      if (!model.dataAxis || !model.typeAxis) {
+        this.$snackbar.error('请选择数据轴和类型轴')
+        return
+      }
+      const { typeAxis, dataAxis } = model
+      const data = {
+        type: typeAxis ? content.rows.map(e => e[typeAxis]) : [],
+        data: dataAxis ? dataAxis.map(e => content.rows.map(r => r[e])) : []
+      }
+      this.$emit('render', data)
     }
   }
 }
@@ -207,4 +381,10 @@ export default {
     }
   }
 }
+
+.position {
+  &-relative {
+    position: relative;
+  }
+}
 </style>