Kaynağa Gözat

Merge branch 'dev' into dev-feat-multi-select

奔跑的面条 3 yıl önce
ebeveyn
işleme
84c9e73e7b
27 değiştirilmiş dosya ile 671 ekleme ve 132 silme
  1. 13 3
      build/constant.ts
  2. 2 0
      package.json
  3. 16 0
      pnpm-lock.yaml
  4. 20 0
      src/components/Pages/MonacoEditor/EditorWorker.vue
  5. 76 0
      src/components/Pages/MonacoEditor/index.hook.ts
  6. 4 0
      src/components/Pages/MonacoEditor/index.ts
  7. 92 0
      src/components/Pages/MonacoEditor/index.vue
  8. 1 1
      src/components/Pages/ThemeColorSelect/index.vue
  9. 14 16
      src/hooks/useChartDataFetch.hook.ts
  10. 5 5
      src/packages/components/Charts/Mores/Process/index.vue
  11. 3 3
      src/packages/components/Charts/Mores/WaterPolo/index.vue
  12. 8 2
      src/packages/index.d.ts
  13. 2 0
      src/packages/public/publicConfig.ts
  14. 5 2
      src/plugins/icon.ts
  15. 3 0
      src/store/modules/chartEditStore/chartEditStore.ts
  16. 1 1
      src/utils/style.ts
  17. 10 0
      src/utils/type.ts
  18. 36 8
      src/utils/utils.ts
  19. 50 41
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataAjax/index.vue
  20. 75 40
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMatchingAndShow/index.vue
  21. 3 0
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMonacoEditor/index.ts
  22. 217 0
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMonacoEditor/index.vue
  23. 1 1
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataStatic/index.vue
  24. 1 0
      src/views/chart/ContentConfigurations/components/ChartData/index.d.ts
  25. 2 2
      src/views/chart/ContentEdit/index.vue
  26. 2 2
      src/views/preview/components/PreviewRenderList/index.vue
  27. 9 5
      vite.config.ts

+ 13 - 3
build/constant.ts

@@ -1,4 +1,7 @@
-export const OUTPUT_DIR = 'dist';
+export const OUTPUT_DIR = 'dist'
+
+// monaco-editor 路径
+export const prefix = `monaco-editor/esm/vs`
 
 // chunk 警告大小
 export const chunkSizeWarningLimit = 2000
@@ -11,7 +14,14 @@ export const rollupOptions = {
   output: {
     chunkFileNames: 'static/js/[name]-[hash].js',
     entryFileNames: 'static/js/[name]-[hash].js',
-    assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
+    assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
+    manualChunks: {
+      jsonWorker: [`${prefix}/language/json/json.worker`],
+      cssWorker: [`${prefix}/language/css/css.worker`],
+      htmlWorker: [`${prefix}/language/html/html.worker`],
+      tsWorker: [`${prefix}/language/typescript/ts.worker`],
+      editorWorker: [`${prefix}/editor/editor.worker`]
+    }
   }
 }
 
@@ -22,4 +32,4 @@ export const terserOptions = {
     drop_console: true,
     drop_debugger: true
   }
-}
+}

+ 2 - 0
package.json

@@ -20,6 +20,7 @@
     "highlight.js": "^11.5.0",
     "html2canvas": "^1.4.1",
     "keymaster": "^1.6.2",
+    "monaco-editor": "^0.33.0",
     "naive-ui": "2.30.3",
     "pinia": "^2.0.13",
     "screenfull": "^6.0.1",
@@ -63,6 +64,7 @@
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-importer": "^0.2.5",
     "vite-plugin-mock": "^2.9.6",
+    "vite-plugin-monaco-editor": "^1.1.0",
     "vue-echarts": "^6.0.2",
     "vue-tsc": "^0.28.10"
   }

+ 16 - 0
pnpm-lock.yaml

@@ -34,6 +34,7 @@ specifiers:
   keymaster: ^1.6.2
   lodash: ~4.17.21
   mockjs: ^1.1.0
+  monaco-editor: ^0.33.0
   naive-ui: 2.30.3
   pinia: ^2.0.13
   plop: ^3.0.5
@@ -46,6 +47,7 @@ specifiers:
   vite-plugin-compression: ^0.5.1
   vite-plugin-importer: ^0.2.5
   vite-plugin-mock: ^2.9.6
+  vite-plugin-monaco-editor: ^1.1.0
   vue: ^3.2.31
   vue-demi: ^0.13.1
   vue-echarts: ^6.0.2
@@ -68,6 +70,7 @@ dependencies:
   highlight.js: 11.5.1
   html2canvas: 1.4.1
   keymaster: 1.6.2
+  monaco-editor: 0.33.0
   naive-ui: 2.30.3_vue@3.2.37
   pinia: 2.0.14_vcmyupim4cga7k7f5hngmth5py
   screenfull: 6.0.1
@@ -111,6 +114,7 @@ devDependencies:
   vite-plugin-compression: 0.5.1_vite@2.9.5
   vite-plugin-importer: 0.2.5
   vite-plugin-mock: 2.9.6_mockjs@1.1.0+vite@2.9.5
+  vite-plugin-monaco-editor: 1.1.0_monaco-editor@0.33.0
   vue-echarts: 6.0.3_echarts@5.3.3+vue@3.2.37
   vue-tsc: 0.28.10_typescript@4.7.3
 
@@ -3756,6 +3760,10 @@ packages:
       commander: 9.3.0
     dev: true
 
+  /monaco-editor/0.33.0:
+    resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==}
+    dev: false
+
   /ms/2.0.0:
     resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
     dev: true
@@ -5105,6 +5113,14 @@ packages:
       - supports-color
     dev: true
 
+  /vite-plugin-monaco-editor/1.1.0_monaco-editor@0.33.0:
+    resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==}
+    peerDependencies:
+      monaco-editor: '>=0.33.0'
+    dependencies:
+      monaco-editor: 0.33.0
+    dev: true
+
   /vite/2.9.5_sass@1.52.3:
     resolution: {integrity: sha512-dvMN64X2YEQgSXF1lYabKXw3BbN6e+BL67+P3Vy4MacnY+UzT1AfkHiioFSi9+uiDUiaDy7Ax/LQqivk6orilg==}
     engines: {node: '>=12.2.0'}

+ 20 - 0
src/components/Pages/MonacoEditor/EditorWorker.vue

@@ -0,0 +1,20 @@
+<template></template>
+
+<script setup>
+import * as monaco from 'monaco-editor'
+import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
+import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
+import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
+
+self.MonacoEnvironment = {
+  getWorker(workerId, label) {
+    if (label === 'json') {
+      return new jsonWorker()
+    }
+    if (label === 'typescript' || label === 'javascript') {
+      return new tsWorker()
+    }
+    return new editorWorker()
+  }
+}
+</script>

+ 76 - 0
src/components/Pages/MonacoEditor/index.hook.ts

@@ -0,0 +1,76 @@
+import { ref, onBeforeUnmount, nextTick } from 'vue'
+import { useDesignStore } from '@/store/modules/designStore/designStore'
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
+
+export const useMonacoEditor = (language = 'javascript') => {
+const designStore = useDesignStore()
+
+  let monacoEditor: monaco.editor.IStandaloneCodeEditor | null = null
+  let initReadOnly = false
+  const el = ref<HTMLElement | null>(null)
+
+  // 格式化
+  const onFormatDoc = async () => {
+    await monacoEditor?.getAction('monacoEditor.action.formatDocument')?.run()
+  }
+
+  // 更新
+  const updateVal = (val: string) => {
+    nextTick(async () => {
+      monacoEditor?.setValue(val)
+      initReadOnly && monacoEditor?.updateOptions({ readOnly: false })
+      await onFormatDoc()
+      initReadOnly && monacoEditor?.updateOptions({ readOnly: true })
+    })
+  }
+
+  // 创建实例
+  const createEditor = (editorOption: monaco.editor.IStandaloneEditorConstructionOptions = {}) => {
+    if (!el.value) return
+    const javascriptModel = monaco.editor.createModel('', language)
+    initReadOnly = !!editorOption.readOnly
+    // 创建
+    monacoEditor = monaco.editor.create(el.value, {
+      model: javascriptModel,
+      // 是否启用预览图
+      minimap: { enabled: false },
+      // 圆角
+      roundedSelection: true,
+      // 主题
+      theme: designStore.getDarkTheme ? 'vs-dark' : 'vs',
+      // 主键
+      multiCursorModifier: 'ctrlCmd',
+      // 滚动条
+      scrollbar: {
+        verticalScrollbarSize: 8,
+        horizontalScrollbarSize: 8
+      },
+      // 行号
+      lineNumbers: 'off',
+      // tab大小
+      tabSize: 2,
+      //字体大小
+      fontSize: 16,
+      // 控制编辑器在用户键入、粘贴、移动或缩进行时是否应自动调整缩进
+      autoIndent: 'advanced',
+      // 自动布局
+      automaticLayout: true,
+      ...editorOption
+    })
+
+    return monacoEditor
+  }
+
+  // 卸载
+  onBeforeUnmount(() => {
+    if (monacoEditor) monacoEditor.dispose()
+  })
+
+  return {
+    el,
+    updateVal,
+    getEditor: () => monacoEditor,
+    createEditor,
+    onFormatDoc
+  }
+}

+ 4 - 0
src/components/Pages/MonacoEditor/index.ts

@@ -0,0 +1,4 @@
+import MonacoEditor from './index.vue';
+import EditorWorker from './EditorWorker.vue';
+
+export { MonacoEditor, EditorWorker };

+ 92 - 0
src/components/Pages/MonacoEditor/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <div ref="el" class="go-editor-area" :style="{ width, height }"></div>
+  <EditorWorker></EditorWorker>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, watch, PropType } from 'vue'
+import { useMonacoEditor } from './index.hook'
+import { EditorWorker } from './index'
+
+const props = defineProps({
+  width: {
+    type: String as PropType<string>,
+    default: '100%'
+  },
+  height: {
+    type: String as PropType<string>,
+    default: '90vh'
+  },
+  language: {
+    type: String as PropType<string>,
+    default: 'typescript'
+  },
+  preComment: {
+    type: String as PropType<string>,
+    default: ''
+  },
+  modelValue: {
+    type: String as PropType<string>,
+    default: ''
+  },
+  editorOptions: {
+    type: Object as PropType<object>,
+    default: () => ({})
+  }
+})
+
+const emits = defineEmits(['blur', 'update:modelValue'])
+
+const { el, updateVal, getEditor, createEditor } = useMonacoEditor(props.language)
+
+const updateMonacoVal = (_val?: string) => {
+  const { modelValue, preComment } = props
+  const val = preComment ? `${preComment}\n${_val || modelValue}` : _val || modelValue
+  updateVal(val)
+}
+
+onMounted(() => {
+  const monacoEditor = createEditor(props.editorOptions)
+  monacoEditor!.onDidChangeModelContent(() => {
+    emits('update:modelValue', monacoEditor!.getValue())
+  })
+  monacoEditor!.onDidBlurEditorText(() => {
+    emits('blur')
+  })
+  updateMonacoVal()
+})
+
+watch(
+  () => props.modelValue,
+  (val: string) => {
+    val !== getEditor()?.getValue() && updateMonacoVal(val)
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+.go-editor-area {
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+  padding: 5px;
+  padding-left: 0;
+  box-sizing: border-box;
+  background-color: rgba(0, 0, 0, 0);
+  @include deep() {
+    .margin,
+    .monaco-editor,
+    .inputarea.ime-input {
+      background-color: rgba(0, 0, 0, 0);
+    }
+
+    .monaco-editor-background {
+      background-color: rgba(0, 0, 0, 0);
+      @include fetch-bg-color('filter-color-shallow');
+    }
+    .margin {
+      @include fetch-bg-color('filter-color-shallow');
+    }
+  }
+}
+</style>

+ 1 - 1
src/components/Pages/ThemeColorSelect/index.vue

@@ -20,7 +20,7 @@
         <div class="content-right">
           <div class="color-name-detail">
             <n-text v-if="appThemeDetail" class="color-name">{{ appThemeDetail.name }}</n-text>
-            <n-text v-else="appThemeDetail" class="color-name">中国色</n-text>
+            <n-text v-else class="color-name">中国色</n-text>
             <n-text
               v-if="appThemeDetail"
               class="color-name-Pinyin"

+ 14 - 16
src/hooks/useChartDataFetch.hook.ts

@@ -4,11 +4,12 @@ import { http } from '@/api/http'
 import { CreateComponentType, ChartFrameEnum } from '@/packages/index.d'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { RequestDataTypeEnum } from '@/enums/httpEnum'
-import { isPreview } from '@/utils'
+import { isPreview, newFunctionHandle } from '@/utils'
 
 // 获取类型
 type ChartEditStoreType = typeof useChartEditStore
 
+
 /**
  * setdata 数据监听与更改
  * @param targetComponent
@@ -23,38 +24,36 @@ export const useChartDataFetch = (
   const vChartRef = ref<typeof VChart | null>(null)
   let fetchInterval: any = 0
 
-  const requestInterval = () => {
+  const requestIntervalFn = () => {
     const chartEditStore = useChartEditStore()
-    const { requestOriginUrl, requestInterval } = toRefs(
-      chartEditStore.getRequestGlobalConfig
-    )
+    const { requestOriginUrl, requestInterval } = toRefs(chartEditStore.getRequestGlobalConfig)
     // 组件类型
     const { chartFrame } = targetComponent.chartConfig
     // 请求配置
-    const { requestDataType, requestHttpType, requestUrl } = toRefs(
-      targetComponent.data
-    )
+    const { requestDataType, requestHttpType, requestUrl } = toRefs(targetComponent.data)
     // 非请求类型
     if (requestDataType.value !== RequestDataTypeEnum.AJAX) return
     // 处理地址
     if (requestUrl?.value && requestInterval.value > 0) {
       // requestOriginUrl 允许为空
-      const completePath =
-        requestOriginUrl && requestOriginUrl.value + requestUrl.value
+      const completePath = requestOriginUrl && requestOriginUrl.value + requestUrl.value
       if (!completePath) return
 
+      clearInterval(fetchInterval)
+
       const fetchFn = async () => {
         const res: any = await http(requestHttpType.value)(completePath || '', {})
         if (res.data) {
           try {
+            const filter = targetComponent.filter
             // 更新回调函数
             if (updateCallback) {
-              updateCallback(res.data)
+              updateCallback(newFunctionHandle(res.data, filter))
             } else {
               // eCharts 组件配合 vChart 库更新方式
               if (chartFrame === ChartFrameEnum.ECHARTS) {
                 if (vChartRef.value) {
-                  vChartRef.value.setOption({ dataset: res.data })
+                  vChartRef.value.setOption({ dataset: newFunctionHandle(res.data, filter) })
                 }
               }
             }
@@ -63,16 +62,15 @@ export const useChartDataFetch = (
           }
         }
       }
-      
+
       // 立即调用
       fetchFn()
       // 开启定时
-      setInterval(fetchFn, requestInterval.value * 1000)
+      fetchInterval = setInterval(fetchFn, requestInterval.value * 1000)
     }
   }
 
-  isPreview() && requestInterval()
-     
+  isPreview() && requestIntervalFn()
 
   return { vChartRef }
 }

+ 5 - 5
src/packages/components/Charts/Mores/Process/index.vue

@@ -15,7 +15,7 @@
         fontSize: `${indicatorTextSize}px`
       }"
     >
-      {{option.dataset}} {{unit}}
+      {{ option.dataset }} {{ unit }}
     </n-text>
   </n-progress>
 </template>
@@ -24,7 +24,8 @@
 import { PropType, toRefs, watch, shallowReactive } from 'vue'
 import { useChartDataFetch } from '@/hooks'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
-import config, { option as configOption  } from './config'
+import config, { option as configOption } from './config'
+import { toNumber } from '@/utils'
 
 const props = defineProps({
   chartConfig: {
@@ -39,7 +40,6 @@ const {
   type,
   unit,
   color,
-  fontSize,
   processing,
   railColor,
   indicatorTextColor,
@@ -57,11 +57,11 @@ const option = shallowReactive({
 watch(
   () => props.chartConfig.option.dataset,
   (newData: any) => {
-    option.dataset = newData
+    option.dataset = toNumber(newData, 2)
   }
 )
 // 预览更新
 useChartDataFetch(props.chartConfig, useChartEditStore, (newData: number) => {
-  option.dataset = newData
+  option.dataset = toNumber(newData, 2)
 })
 </script>

+ 3 - 3
src/packages/components/Charts/Mores/WaterPolo/index.vue

@@ -10,7 +10,7 @@ import 'echarts-liquidfill/src/liquidFill.js'
 import { CanvasRenderer } from 'echarts/renderers'
 import { GridComponent } from 'echarts/components'
 import config from './config'
-import { isPreview } from '@/utils'
+import { isPreview, isString } from '@/utils'
 import { chartColorsSearch, defaultTheme } from '@/settings/chartThemes/index'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { useChartDataFetch } from '@/hooks'
@@ -66,7 +66,8 @@ watch(
 )
 
 // 数据处理
-const dataHandle = (newData: number) => {
+const dataHandle = (newData: number | string) => {
+  newData = isString(newData) ? parseFloat(newData) : newData
   return parseFloat(newData.toFixed(2))
 }
 
@@ -74,7 +75,6 @@ const dataHandle = (newData: number) => {
 watch(
   () => props.chartConfig.option.dataset,
   newData => {
-    console.log(dataHandle(newData))
     props.chartConfig.option.series[0].data = [dataHandle(newData)]
     option.value = props.chartConfig.option
   },

+ 8 - 2
src/packages/index.d.ts

@@ -2,9 +2,14 @@ import type { GlobalThemeJsonType } from '@/settings/chartThemes/index'
 import type { RequestConfigType } from '@/store/modules/chartEditStore/chartEditStore.d'
 
 export enum ChartFrameEnum {
-  COMMON = 'common',
+  // echarts 框架
   ECHARTS = 'echarts',
-  NAIVE_UI = 'naiveUI'
+  // UI 组件框架
+  NAIVE_UI = 'naiveUI',
+  // 自定义带数据组件
+  COMMON = 'common',
+  // 无数据变更
+  STATIC = 'static'
 }
 
 // 组件配置
@@ -77,6 +82,7 @@ export interface PublicConfigType extends requestConfig {
     // 动画
     animations: string[]
   }
+  filter?: string
   setPosition: Function
 }
 

+ 2 - 0
src/packages/public/publicConfig.ts

@@ -45,6 +45,8 @@ export class publicConfig implements PublicConfigType {
   public data = { ...requestConfig }
   // 数据获取
   public requestData = []
+  // 数据过滤
+  public filter = undefined
 
   // 设置坐标
   public setPosition(x: number, y: number): void {

+ 5 - 2
src/plugins/icon.ts

@@ -52,7 +52,8 @@ import {
   ArrowBack as ArrowBackIcon,
   ArrowForward as ArrowForwardIcon,
   Planet as PawIcon,
-  Search as SearchIcon
+  Search as SearchIcon,
+  Filter as FilterIcon
 } from '@vicons/ionicons5'
 
 import {
@@ -192,7 +193,9 @@ const ionicons5 = {
   // 狗爪
   PawIcon,
   // 搜索(放大镜)
-  SearchIcon
+  SearchIcon,
+  // 过滤器
+  FilterIcon
 }
 
 const carbon = {

+ 3 - 0
src/store/modules/chartEditStore/chartEditStore.ts

@@ -167,6 +167,9 @@ export const useChartEditStore = defineStore({
     },
     // * 设置目标数据 select
     setTargetSelectChart(selectId?: string | string[], push: boolean = false) {
+      // 重复选中
+      if(this.targetChart.selectId.find((e: string) => e === selectId)) return
+
       // 无 id 清空
       if(!selectId) {
         this.targetChart.selectId = []

+ 1 - 1
src/utils/style.ts

@@ -24,7 +24,7 @@ export const getFilterStyle = (styles: StylesType | EditCanvasConfigType) => {
 }
 
 // 变换
-export const getTranstormStyle = (styles: StylesType) => {
+export const getTransformStyle = (styles: StylesType) => {
   const { rotateZ, rotateX, rotateY, skewX, skewY } = styles
   return {
     transform: `rotateZ(${rotateZ || 0}deg) rotateX(${rotateX || 0}deg) rotateY(${rotateY || 0}deg) skewX(${skewX || 0}deg) skewY(${skewY || 0}deg)`,

+ 10 - 0
src/utils/type.ts

@@ -1,3 +1,5 @@
+import isObject from 'lodash/isObject'
+
 export function isString(p: any): p is string {
   return typeof p === 'string'
 }
@@ -21,3 +23,11 @@ export function isNull(p: any): p is null {
 export function isArray(p: any): p is [] {
   return Array.isArray(p)
 }
+
+export const toNumber = (number: number | string, toFixedNumber = 2) => {
+  return isString(number) ? parseFloat(parseFloat(number).toFixed(2)) : number
+}
+
+export const toString = (str: any) => {
+  return isNumber(str) ? `${str}` : (isObject(str) ? JSON.stringify(str) : str)
+}

+ 36 - 8
src/utils/utils.ts

@@ -5,6 +5,8 @@ import throttle from 'lodash/throttle'
 import Image_404 from '../assets/images/exception/image-404.png'
 import html2canvas from 'html2canvas'
 import { downloadByA } from './file'
+import { toString } from './type'
+import cloneDeep from 'lodash/cloneDeep';
 
 /**
  * * 判断是否是开发环境
@@ -19,9 +21,7 @@ export const isDev = () => {
  * @param { Number } randomLength
  */
 export const getUUID = (randomLength = 10) => {
-  return Number(
-    Math.random().toString().substr(2, randomLength) + Date.now()
-  ).toString(36)
+  return Number(Math.random().toString().substr(2, randomLength) + Date.now()).toString(36)
 }
 
 /**
@@ -90,10 +90,7 @@ export const screenfullFn = (isFullscreen?: boolean, isEnabled?: boolean) => {
  * @param key 键名
  * @param value 键值
  */
-export const setDomAttribute = <
-  K extends keyof CSSStyleDeclaration,
-  V extends CSSStyleDeclaration[K]
->(
+export const setDomAttribute = <K extends keyof CSSStyleDeclaration, V extends CSSStyleDeclaration[K]>(
   HTMLElement: HTMLElement,
   key: K,
   value: V
@@ -149,7 +146,7 @@ export const addEventListener = <K extends keyof WindowEventMap>(
     type,
     throttle(listener, delay || 300, {
       leading: true,
-      trailing: false,
+      trailing: false
     }),
     options
   )
@@ -186,3 +183,34 @@ export const canvasCut = (html: HTMLElement | null, callback?: Function) => {
     if (callback) callback()
   })
 }
+
+/**
+ * * 函数过滤器
+ * @param data 数据值
+ * @param funcStr 函数字符串
+ * @param toString 转为字符串
+ * @param errorCallBack 错误回调函数
+ * @param successCallBack 成功回调函数
+ * @returns
+ */
+export const newFunctionHandle = (
+  data: any,
+  funcStr?: string,
+  isToString?: boolean,
+  errorCallBack?: Function,
+  successCallBack?: Function
+) => {
+  try {
+    if (!funcStr) return data
+    const fn = new Function('data', funcStr)
+    const fnRes = fn( cloneDeep(data))
+    const resHandle = isToString ? toString(fnRes) : fnRes
+    // 成功回调
+    successCallBack && successCallBack(resHandle)
+    return resHandle
+  } catch (error) {
+    // 失败回调
+    errorCallBack && errorCallBack(error)
+    return '函数执行错误'
+  }
+}

+ 50 - 41
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataAjax/index.vue

@@ -1,10 +1,7 @@
 <template>
   <div class="go-chart-configurations-data-ajax">
     <setting-item-box name="类型" :alone="true">
-      <n-select
-        v-model:value="targetData.data.requestHttpType"
-        :options="selectOptions"
-      />
+      <n-select v-model:value="targetData.data.requestHttpType" :options="selectOptions" />
     </setting-item-box>
 
     <setting-item-box name="源地址:" :alone="true">
@@ -23,19 +20,26 @@
           <ul class="go-pl-0">
             开发环境使用 mock 数据,请输入
             <li v-for="item in apiList" :key="item.value">
-              <n-text type="info"> {{item.value}} </n-text>
+              <n-text type="info"> {{ item.value }} </n-text>
             </li>
           </ul>
         </n-tooltip>
       </template>
-      <n-input
-        v-model:value.trim="targetData.data.requestUrl"
-        :min="1"
-        placeholder="请输入地址(去除源)"
-      />
+      <n-input v-model:value.trim="targetData.data.requestUrl" :min="1" placeholder="请输入地址(去除源)" />
     </setting-item-box>
 
-    <setting-item-box name="测试" :alone="true">
+    <setting-item-box :alone="true">
+      <template #name>
+        测试
+        <n-tooltip trigger="hover">
+          <template #trigger>
+            <n-icon size="21" :depth="3">
+              <help-outline-icon></help-outline-icon>
+            </n-icon>
+          </template>
+          默认赋值给 dataset 字段
+        </n-tooltip>
+      </template>
       <n-space>
         <n-button @click="sendHandle">
           <template #icon>
@@ -48,17 +52,13 @@
       </n-space>
     </setting-item-box>
 
+    <chart-data-matching-and-show :show="showMatching && !loading" :ajax="true"></chart-data-matching-and-show>
     <go-skeleton :load="loading" :repeat="3"></go-skeleton>
-
-    <chart-data-matching-and-show
-      v-show="showMatching && !loading"
-      :ajax="true"
-    ></chart-data-matching-and-show>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs } from 'vue'
+import { ref, toRefs, onBeforeUnmount, watchEffect } from 'vue'
 import { icon } from '@/plugins'
 import { SettingItemBox } from '@/components/Pages/ChartItemSetting'
 import { RequestHttpEnum, ResultEnum } from '@/enums/httpEnum'
@@ -67,56 +67,56 @@ import { http } from '@/api/http'
 import { SelectHttpType } from '../../index.d'
 import { ChartDataMatchingAndShow } from '../ChartDataMatchingAndShow'
 import { useTargetData } from '../../../hooks/useTargetData.hook'
-import { isDev } from '@/utils'
+import { isDev, newFunctionHandle } from '@/utils'
 
 const { HelpOutlineIcon, FlashIcon } = icon.ionicons5
-
 const { targetData, chartEditStore } = useTargetData()
 const { requestOriginUrl } = toRefs(chartEditStore.getRequestGlobalConfig)
 // 是否展示数据分析
 const loading = ref(false)
 const showMatching = ref(false)
+let lastFilter: any = undefined
 
 const apiList = [
   {
-    value: `【图表】${ chartDataUrl }`
+    value: `【图表】${chartDataUrl}`
   },
   {
-    value: `【文本】${ textUrl }`
+    value: `【文本】${textUrl}`
   },
   {
-    value: `【0~100 整数】${ numberIntUrl }`
+    value: `【0~100 整数】${numberIntUrl}`
   },
   {
-    value: `【0~1小数】${ numberFloatUrl }`
+    value: `【0~1小数】${numberFloatUrl}`
   },
   {
-    value: `【图片地址】${ imageUrl }`
+    value: `【图片地址】${imageUrl}`
   },
   {
-    value: `【排名列表】${ rankListUrl }`
+    value: `【排名列表】${rankListUrl}`
   },
   {
-    value: `【滚动表格】${ scrollBoardUrl }`
-  },
-    
+    value: `【滚动表格】${scrollBoardUrl}`
+  }
 ]
 
 // 选项
 const selectOptions: SelectHttpType[] = [
   {
     label: RequestHttpEnum.GET,
-    value: RequestHttpEnum.GET,
+    value: RequestHttpEnum.GET
   },
   {
     label: RequestHttpEnum.POST,
-    value: RequestHttpEnum.POST,
-  },
+    value: RequestHttpEnum.POST
+  }
 ]
 
 // 发送请求
 const sendHandle = async () => {
   loading.value = true
+  if(!targetData.value) return
   const { requestUrl, requestHttpType } = targetData.value.data
   if (!requestUrl) {
     window['$message'].warning('请求参数不正确,请检查!')
@@ -124,17 +124,26 @@ const sendHandle = async () => {
   }
   const completePath = requestOriginUrl && requestOriginUrl.value + requestUrl
   const res = await http(requestHttpType)(completePath || '', {})
-  setTimeout(() => {
-    loading.value = false
-    if (res.status === ResultEnum.SUCCESS) {
-      // @ts-ignore
-      targetData.value.option.dataset = res.data
-      showMatching.value = true
-      return
-    }
-    window['$message'].warning('数据异常,请检查接口数据!')
-  }, 500)
+  loading.value = false
+  if (res.status === ResultEnum.SUCCESS) {
+    targetData.value.option.dataset = newFunctionHandle(res.data, targetData.value.filter)
+    showMatching.value = true
+    return
+  }
+  window['$message'].warning('数据异常,请检查接口数据!')
 }
+
+watchEffect(() => {
+  const filter = targetData.value?.filter
+  if (lastFilter !== filter) {
+    lastFilter = filter
+    sendHandle()
+  }
+})
+
+onBeforeUnmount(() => {
+  lastFilter = null
+})
 </script>
 
 <style lang="scss" scoped>

+ 75 - 40
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMatchingAndShow/index.vue

@@ -1,6 +1,6 @@
 <template>
   <n-timeline class="go-chart-configurations-timeline">
-    <n-timeline-item v-if="isCharts && dimensionsAndSource" type="info" :title="TimelineTitleEnum.MAPPING">
+    <n-timeline-item v-show="isCharts && dimensionsAndSource" type="info" :title="TimelineTitleEnum.MAPPING">
       <n-table striped>
         <thead>
           <tr>
@@ -25,6 +25,12 @@
         </tbody>
       </n-table>
     </n-timeline-item>
+    <n-timeline-item v-show="filterShow" color="#97846c" :title="TimelineTitleEnum.FILTER">
+      <n-space vertical>
+        <n-text depth="3">点击{{ targetData.filter ? '「编辑」' : '「新增」' }}查看过滤规则</n-text>
+        <chart-data-monaco-editor></chart-data-monaco-editor>
+      </n-space>
+    </n-timeline-item>
     <n-timeline-item type="success" :title="TimelineTitleEnum.CONTENT">
       <n-space vertical>
         <n-text depth="3">ECharts 图表需符合 ECharts-setdata 数据规范</n-text>
@@ -55,7 +61,7 @@
               </template>
               下载
             </n-button>
-           <n-tooltip trigger="hover">
+            <n-tooltip trigger="hover">
               <template #trigger>
                 <n-icon class="go-ml-1" size="21" :depth="3">
                   <help-outline-icon></help-outline-icon>
@@ -65,8 +71,8 @@
             </n-tooltip>
           </div>
         </n-space>
-        <n-card>
-          <n-code :code="getSource" language="json"></n-code>
+        <n-card size="small">
+          <n-code :code="filterRes(source)" language="json"></n-code>
         </n-card>
       </n-space>
     </n-timeline-item>
@@ -76,14 +82,22 @@
 <script setup lang="ts">
 import { ref, computed, watch } from 'vue'
 import { CreateComponentType, PackagesCategoryEnum } from '@/packages/index.d'
+import { RequestDataTypeEnum } from '@/enums/httpEnum'
 import { icon } from '@/plugins'
 import { DataResultEnum, TimelineTitleEnum } from '../../index.d'
+import { ChartDataMonacoEditor } from '../ChartDataMonacoEditor'
 import { useFile } from '../../hooks/useFile.hooks'
 import { useTargetData } from '../../../hooks/useTargetData.hook'
 import isObject from 'lodash/isObject'
-const { targetData } = useTargetData()
+import cloneDeep from 'lodash/cloneDeep'
+import { toString } from '@/utils'
 
+const { targetData } = useTargetData()
 const props = defineProps({
+  show: {
+    type: Boolean,
+    required: false
+  },
   ajax: {
     type: Boolean,
     required: true
@@ -102,16 +116,16 @@ const dimensionsAndSource = ref()
 
 const { uploadFileListRef, customRequest, beforeUpload, download } = useFile(targetData)
 
+// 是否展示过滤器
+const filterShow = computed(() => {
+  return targetData.value.data.requestDataType === RequestDataTypeEnum.AJAX
+})
+
 // 是图表类型
 const isCharts = computed(() => {
   return targetData.value.chartConfig.package === PackagesCategoryEnum.CHARTS
 })
 
-// 获取数据
-const getSource = computed(() => {
-  return JSON.stringify(source.value)
-})
-
 // 处理映射列表状态结果
 const matchingHandle = (mapping: string) => {
   let res = DataResultEnum.SUCCESS
@@ -129,47 +143,68 @@ const dimensionsAndSourceHandle = () => {
   try {
     // 去除首项数据轴标识
     return dimensions.value.map((dimensionsItem: string, index: number) => {
-      return index === 0 ?
-        {
-          // 字段
-          field: '通用标识',
-          // 映射
-          mapping: dimensionsItem,
-          // 结果
-          result: DataResultEnum.NULL
-        } : {
-          field: `数据项-${index}`,
-          mapping: dimensionsItem,
-          result: matchingHandle(dimensionsItem)
-        }
+      return index === 0
+        ? {
+            // 字段
+            field: '通用标识',
+            // 映射
+            mapping: dimensionsItem,
+            // 结果
+            result: DataResultEnum.NULL
+          }
+        : {
+            field: `数据项-${index}`,
+            mapping: dimensionsItem,
+            result: matchingHandle(dimensionsItem)
+          }
     })
   } catch (error) {
     return []
   }
 }
 
-watch(() => targetData.value?.option?.dataset, (newData: {
-  source: any,
-  dimensions: any
-} | null) => {
-  if (newData && isObject(newData)) {
-    // 只有 Echarts 数据才有对应的格式
-    source.value = isCharts.value ? newData.source : newData
-    if (isCharts.value) {
-      dimensions.value = newData.dimensions
-      dimensionsAndSource.value = dimensionsAndSourceHandle()
+// 过滤结果
+const filterRes = (data: any) => {
+  try {
+    if(targetData.value.filter) {
+      const fn = new Function('data', targetData.value.filter)
+      const res = fn(cloneDeep(data))
+      return toString(res)
     }
-  } else {
-    dimensionsAndSource.value = null
-    source.value = newData
+    return toString(cloneDeep(data))
+  } catch (error) {
+    return '过滤函数错误'
   }
-}, {
-  immediate: true
-})
+}
+
+watch(
+  () => targetData.value?.option?.dataset,
+  (
+    newData: {
+      source: any
+      dimensions: any
+    } | null
+  ) => {
+    if (newData && isObject(newData)) {
+      // 只有 Echarts 数据才有对应的格式
+      source.value = newData
+      if (isCharts.value) {
+        dimensions.value = newData.dimensions
+        dimensionsAndSource.value = dimensionsAndSourceHandle()
+      }
+    } else {
+      dimensionsAndSource.value = null
+      source.value = newData
+    }
+  },
+  {
+    immediate: true
+  }
+)
 </script>
 
 <style lang="scss" scoped>
-@include go("chart-configurations-timeline") {
+@include go('chart-configurations-timeline') {
   @include deep() {
     pre {
       white-space: pre-wrap;

+ 3 - 0
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMonacoEditor/index.ts

@@ -0,0 +1,3 @@
+import  ChartDataMonacoEditor from './index.vue'
+
+export { ChartDataMonacoEditor }

+ 217 - 0
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataMonacoEditor/index.vue

@@ -0,0 +1,217 @@
+<template>
+  <template v-if="targetData.filter">
+    <n-card>
+      <p><span class="func-keyword">function</span>&nbsp;&nbsp;filter(data)&nbsp;&nbsp;{</p>
+      <!-- 函数体 -->
+      <div class="go-ml-4">
+        <n-code :code="targetData.filter" language="typescript"></n-code>
+      </div>
+      <p>}</p>
+      <template #footer>
+        <n-space justify="end">
+          <n-button tertiary size="small" @click="delFilter">
+            <template #icon>
+              <n-icon>
+                <trash-icon />
+              </n-icon>
+            </template>
+            删除
+          </n-button>
+          <n-button type="info" tertiary size="small" @click="addFilter">
+            <template #icon>
+              <n-icon>
+                <pencil-icon />
+              </n-icon>
+            </template>
+            编辑
+          </n-button>
+        </n-space>
+      </template>
+    </n-card>
+  </template>
+  <template v-else>
+    <n-space justify="space-around">
+      <n-button @click="addFilter">
+        <template #icon>
+          <n-icon>
+            <filter-icon />
+          </n-icon>
+        </template>
+        新增过滤器
+      </n-button>
+    </n-space>
+  </template>
+
+  <!-- 弹窗 -->
+  <n-modal class="go-chart-data-monaco-editor" v-model:show="showModal" :mask-closable="false">
+    <n-card :bordered="false" role="dialog" size="small" aria-modal="true" style="width: 1000px; height: 600px">
+      <template #header>
+        <n-space>
+          <n-text>过滤器函数编辑器</n-text>
+        </n-space>
+      </template>
+      <template #header-extra> </template>
+      <n-space size="small" vertical>
+        <n-space justify="space-between">
+          <div>
+            <n-space vertical>
+              <n-tag type="info">
+                <span class="func-keyword">function</span>&nbsp;&nbsp;filter(data)&nbsp;&nbsp;{
+              </n-tag>
+              <monaco-editor v-model:modelValue="filter" width="460px" height="380px" language="javascript" />
+              <n-tag type="info">}</n-tag>
+            </n-space>
+          </div>
+          <n-divider vertical style="height: 480px" />
+          <n-scrollbar style="max-height: 480px">
+            <n-space :size="15" vertical>
+              <div class="editor-data-show">
+                <n-space>
+                  <n-text depth="3">目标数据:</n-text>
+                  <n-code :code="toString(sourceData)" language="json" :word-wrap="true"></n-code>
+                </n-space>
+              </div>
+              <div class="editor-data-show">
+                <n-space>
+                  <n-text depth="3">过滤器结果:</n-text>
+                  <n-code :code="filterRes" language="json" :word-wrap="true"></n-code>
+                </n-space>
+              </div>
+            </n-space>
+          </n-scrollbar>
+        </n-space>
+      </n-space>
+      <template #action>
+        <n-space justify="space-between">
+          <div class="go-flex-items-center">
+            <n-tag :bordered="false" type="success">
+              <template #icon>
+                <n-icon :component="DocumentTextIcon" />
+              </template>
+              规则
+            </n-tag>
+            <n-text class="go-ml-2" depth="2">过滤器将对接口返回值的「data」字段进行处理</n-text>
+          </div>
+
+          <n-space>
+            <n-button size="medium" @click="closeFilter">取消</n-button>
+            <n-button size="medium" type="primary" @click="saveFilter">保存</n-button>
+          </n-space>
+        </n-space>
+      </template>
+    </n-card>
+  </n-modal>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, toRefs } from 'vue'
+import { MonacoEditor } from '@/components/Pages/MonacoEditor'
+import { useTargetData } from '../../../hooks/useTargetData.hook'
+import { RequestHttpEnum, RequestDataTypeEnum, ResultEnum } from '@/enums/httpEnum'
+import { icon } from '@/plugins'
+import { goDialog, toString } from '@/utils'
+import { http } from '@/api/http'
+import cloneDeep from 'lodash/cloneDeep'
+
+const { PencilIcon, TrashIcon, FilterIcon, DocumentTextIcon } = icon.ionicons5
+const { targetData, chartEditStore } = useTargetData()
+const { requestDataType } = toRefs(targetData.value.data)
+const { requestOriginUrl } = toRefs(chartEditStore.getRequestGlobalConfig)
+
+// 受控弹窗
+const showModal = ref(false)
+// filter 函数模板
+const filter = ref(targetData.value.filter || `return data`)
+// 过滤错误标识
+const errorFlag = ref(false)
+// 目标静态/接口数据
+const sourceData = ref<any>('')
+
+// 动态获取数据
+const fetchTargetData = async () => {
+  try {
+    const { requestUrl, requestHttpType } = targetData.value.data
+    if (!requestUrl) {
+      window['$message'].warning('请求参数不正确,请检查!')
+      sourceData.value = '请求参数不正确,请检查!'
+      return
+    }
+    const completePath = requestOriginUrl && requestOriginUrl.value + requestUrl
+    const res = await http(requestHttpType)(completePath || '', {})
+    if (res.status === ResultEnum.SUCCESS) {
+      sourceData.value = res.data
+      return
+    }
+  } catch (error) {
+    window['$message'].warning('数据异常,请检查接口数据!')
+  }
+}
+
+// 过滤结果
+const filterRes = computed(() => {
+  try {
+    const fn = new Function('data', filter.value)
+    const res = fn(cloneDeep(sourceData.value))
+    errorFlag.value = false
+    return toString(res)
+  } catch (error) {
+    errorFlag.value = true
+    return '过滤函数错误'
+  }
+})
+
+// 新增过滤器
+const addFilter = () => {
+  showModal.value = true
+}
+
+// 删除过滤器
+const delFilter = () => {
+  goDialog({
+    message: '是否删除过滤器',
+    onPositiveCallback: () => {
+      targetData.value.filter = undefined
+    }
+  })
+}
+
+// 关闭过滤器
+const closeFilter = () => {
+  showModal.value = false
+}
+
+// 新增过滤器
+const saveFilter = () => {
+  if (errorFlag.value) {
+    window['$message'].error('过滤函数错误,无法进行保存')
+    return
+  }
+  targetData.value.filter = filter.value
+  closeFilter()
+}
+
+watch(
+  () => showModal.value,
+  (newData: boolean) => {
+    if (newData) fetchTargetData()
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+.func-keyword {
+  color: #b478cf;
+}
+@include go('chart-data-monaco-editor') {
+  &.n-card.n-modal,
+  .n-card {
+    @extend .go-background-filter;
+  }
+  .editor-data-show {
+    @include fetch-bg-color('filter-color');
+    width: 420px;
+    padding: 20px;
+    border-radius: 5px;
+  }
+}
+</style>

+ 1 - 1
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataStatic/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="go-chart-configurations-data-static">
-    <chart-data-matching-and-show :ajax="false"></chart-data-matching-and-show>
+    <chart-data-matching-and-show :show="false" :ajax="false"></chart-data-matching-and-show>
   </div>
 </template>
 

+ 1 - 0
src/views/chart/ContentConfigurations/components/ChartData/index.d.ts

@@ -8,6 +8,7 @@ export enum DataResultEnum {
 }
 
 export enum TimelineTitleEnum {
+  FILTER = '数据过滤',
   MAPPING = '数据映射',
   CONTENT = '数据内容',
 }

+ 2 - 2
src/views/chart/ContentEdit/index.vue

@@ -43,7 +43,7 @@
               :style="{
                 ...useSizeStyle(item.attr),
                 ...getFilterStyle(item.styles),
-                ...getTranstormStyle(item.styles),
+                ...getTransformStyle(item.styles),
               }"
             ></component>
           </edit-shape-box>
@@ -66,7 +66,7 @@
 <script lang="ts" setup>
 import { onMounted, computed } from 'vue'
 import { chartColors } from '@/settings/chartThemes/index'
-import { animationsClass, getFilterStyle, getTranstormStyle } from '@/utils'
+import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
 import { useContextMenu } from '@/views/chart/hooks/useContextMenu.hook'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 

+ 2 - 2
src/views/preview/components/PreviewRenderList/index.vue

@@ -7,7 +7,7 @@
     :style="{ 
       ...getComponentAttrStyle(item.attr, index),
       ...getFilterStyle(item.styles),
-      ...getTranstormStyle(item.styles)
+      ...getTransformStyle(item.styles)
     }"
   >
     <component
@@ -24,7 +24,7 @@
 import { PropType, computed } from 'vue'
 import { ChartEditStorageType } from '../../index.d'
 import { chartColors } from '@/settings/chartThemes/index'
-import { animationsClass, getFilterStyle, getTranstormStyle } from '@/utils'
+import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
 import { getSizeStyle, getComponentAttrStyle } from '../../utils'
 
 const props = defineProps({

+ 9 - 5
vite.config.ts

@@ -3,7 +3,8 @@ import vue from '@vitejs/plugin-vue'
 import { resolve } from 'path'
 import { OUTPUT_DIR, brotliSize, chunkSizeWarningLimit, terserOptions, rollupOptions } from './build/constant'
 import viteCompression from 'vite-plugin-compression'
-import { viteMockServe} from "vite-plugin-mock";
+import { viteMockServe } from 'vite-plugin-mock'
+import monacoEditorPlugin from 'vite-plugin-monaco-editor'
 
 function pathResolve(dir: string) {
   return resolve(process.cwd(), '.', dir)
@@ -36,17 +37,20 @@ export default defineConfig({
   },
   plugins: [
     vue(),
+    monacoEditorPlugin({
+      languageWorkers: ['editorWorkerService', 'typescript', 'json']
+    }),
     viteMockServe({
-			mockPath: "/src/api/mock",
+      mockPath: '/src/api/mock',
       // 开发打包开关
-			localEnabled: true,
+      localEnabled: true,
       // 生产打包开关
       prodEnabled: true,
       // 打开后,可以读取 ts 文件模块。 请注意,打开后将无法监视.js 文件
       supportTs: true,
       // 监视文件更改
-      watchFiles: true,
-		}),
+      watchFiles: true
+    }),
     // 压缩
     viteCompression({
       verbose: true,