Переглянути джерело

feat: new component jsonViewer (#5544)

* 添加新组件JsonViewer用于展示JSON结构数据
Netfan 2 місяців тому
батько
коміт
6cba181fad

+ 1 - 0
packages/effects/common-ui/package.json

@@ -35,6 +35,7 @@
     "qrcode": "catalog:",
     "tippy.js": "catalog:",
     "vue": "catalog:",
+    "vue-json-viewer": "catalog:",
     "vue-router": "catalog:",
     "vue-tippy": "catalog:"
   },

+ 1 - 0
packages/effects/common-ui/src/components/index.ts

@@ -3,6 +3,7 @@ export * from './captcha';
 export * from './col-page';
 export * from './ellipsis-text';
 export * from './icon-picker';
+export * from './json-viewer';
 export * from './page';
 export * from './resize';
 export * from './tippy';

+ 3 - 0
packages/effects/common-ui/src/components/json-viewer/index.ts

@@ -0,0 +1,3 @@
+export { default as JsonViewer } from './index.vue';
+
+export * from './types';

+ 77 - 0
packages/effects/common-ui/src/components/json-viewer/index.vue

@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { SetupContext } from 'vue';
+
+import type { Recordable } from '@vben/types';
+
+import type { JsonViewerProps } from './types';
+
+import { computed, useAttrs } from 'vue';
+// @ts-ignore
+import VueJsonViewer from 'vue-json-viewer';
+
+import { $t } from '@vben/locales';
+
+import { isBoolean, isString } from '@vben-core/shared/utils';
+
+defineOptions({ name: 'JsonViewer' });
+
+const props = withDefaults(defineProps<JsonViewerProps>(), {
+  expandDepth: 1,
+  copyable: false,
+  sort: false,
+  boxed: false,
+  theme: 'default-json-theme',
+  expanded: false,
+  previewMode: false,
+  showArrayIndex: true,
+  showDoubleQuotes: false,
+  parseString: true,
+});
+
+const emit = defineEmits<{
+  parseError: [error: Error];
+}>();
+
+const attrs: SetupContext['attrs'] = useAttrs();
+
+const bindProps = computed<Recordable<any>>(() => {
+  const copyable = {
+    copyText: $t('ui.jsonViewer.copy'),
+    copiedText: $t('ui.jsonViewer.copied'),
+    timeout: 2000,
+    ...(isBoolean(props.copyable) ? {} : props.copyable),
+  };
+
+  return {
+    ...props,
+    ...attrs,
+    copyable: props.copyable ? copyable : false,
+  };
+});
+
+const modelValue = defineModel();
+
+const jsonToShow = computed(() => {
+  if (props.parseString && isString(modelValue.value)) {
+    try {
+      return JSON.parse(modelValue.value);
+    } catch (error) {
+      emit('parseError', error as Error);
+      console.error('Error parsing JSON:', error);
+      return modelValue.value;
+    }
+  } else {
+    return modelValue.value;
+  }
+});
+</script>
+<template>
+  <VueJsonViewer :value="jsonToShow" v-bind="bindProps">
+    <template #copy="slotProps">
+      <slot name="copy" v-bind="slotProps"></slot>
+    </template>
+  </VueJsonViewer>
+</template>
+<style lang="scss">
+@use './style.scss';
+</style>

+ 98 - 0
packages/effects/common-ui/src/components/json-viewer/style.scss

@@ -0,0 +1,98 @@
+.default-json-theme {
+  font-family: Consolas, Menlo, Courier, monospace;
+  font-size: 14px;
+  color: hsl(var(--foreground));
+  white-space: nowrap;
+  background: hsl(var(--background));
+
+  &.jv-container.boxed {
+    border: 1px solid hsl(var(--border));
+  }
+
+  .jv-ellipsis {
+    display: inline-block;
+    padding: 0 4px 2px;
+    font-size: 0.9em;
+    line-height: 0.9;
+    color: hsl(var(--secondary-foreground));
+    vertical-align: 2px;
+    cursor: pointer;
+    user-select: none;
+    background-color: hsl(var(--secondary));
+    border-radius: 3px;
+  }
+
+  .jv-button {
+    color: hsl(var(--primary));
+  }
+
+  .jv-key {
+    color: hsl(var(--heavy-foreground));
+  }
+
+  .jv-item {
+    &.jv-array {
+      color: hsl(var(--heavy-foreground));
+    }
+
+    &.jv-boolean {
+      color: hsl(var(--red-400));
+    }
+
+    &.jv-function {
+      color: hsl(var(--destructive-foreground));
+    }
+
+    &.jv-number {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-number-float {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-number-integer {
+      color: hsl(var(--info-foreground));
+    }
+
+    &.jv-object {
+      color: hsl(var(--accent-darker));
+    }
+
+    &.jv-undefined {
+      color: hsl(var(--secondary-foreground));
+    }
+
+    &.jv-string {
+      color: hsl(var(--primary));
+      word-break: break-word;
+      white-space: normal;
+    }
+  }
+
+  &.jv-container .jv-code {
+    padding: 10px;
+
+    &.boxed:not(.open) {
+      padding-bottom: 20px;
+      margin-bottom: 10px;
+    }
+
+    &.open {
+      padding-bottom: 10px;
+    }
+
+    .jv-toggle {
+      &::before {
+        padding: 0 2px;
+        border-radius: 2px;
+      }
+
+      &:hover {
+        &::before {
+          background: hsl(var(--accent-foreground));
+        }
+      }
+    }
+  }
+}

+ 24 - 0
packages/effects/common-ui/src/components/json-viewer/types.ts

@@ -0,0 +1,24 @@
+export interface JsonViewerProps {
+  /** 展开深度 */
+  expandDepth?: number;
+  /** 是否可复制 */
+  copyable?: boolean;
+  /** 是否排序 */
+  sort?: boolean;
+  /** 显示边框 */
+  boxed?: boolean;
+  /** 主题 */
+  theme?: string;
+  /** 是否展开 */
+  expanded?: boolean;
+  /** 时间格式化函数 */
+  timeformat?: (time: Date | number | string) => string;
+  /** 预览模式 */
+  previewMode?: boolean;
+  /** 显示数组索引 */
+  showArrayIndex?: boolean;
+  /** 显示双引号 */
+  showDoubleQuotes?: boolean;
+  /** 解析字符串 */
+  parseString?: boolean;
+}

+ 4 - 0
packages/locales/src/langs/en-US/ui.json

@@ -25,6 +25,10 @@
     "placeholder": "Select an icon",
     "search": "Search icon..."
   },
+  "jsonViewer": {
+    "copy": "Copy",
+    "copied": "Copied"
+  },
   "fallback": {
     "pageNotFound": "Oops! Page Not Found",
     "pageNotFoundDesc": "Sorry, we couldn't find the page you were looking for.",

+ 4 - 0
packages/locales/src/langs/zh-CN/ui.json

@@ -25,6 +25,10 @@
     "placeholder": "选择一个图标",
     "search": "搜索图标..."
   },
+  "jsonViewer": {
+    "copy": "复制",
+    "copied": "已复制"
+  },
   "fallback": {
     "pageNotFound": "哎呀!未找到页面",
     "pageNotFoundDesc": "抱歉,我们无法找到您要找的页面。",

+ 9 - 0
playground/src/router/routes/modules/examples.ts

@@ -255,6 +255,15 @@ const routes: RouteRecordRaw[] = [
           title: 'Tippy',
         },
       },
+      {
+        name: 'JsonViewer',
+        path: '/examples/json-viewer',
+        component: () => import('#/views/examples/json-viewer/index.vue'),
+        meta: {
+          icon: 'tabler:json',
+          title: 'JsonViewer',
+        },
+      },
     ],
   },
 ];

+ 51 - 0
playground/src/views/examples/json-viewer/data.ts

@@ -0,0 +1,51 @@
+export const json1 = {
+  additionalInfo: {
+    author: 'Your Name',
+    debug: true,
+    version: '1.3.10',
+    versionCode: 132,
+  },
+  additionalNotes: 'This JSON is used for demonstration purposes',
+  tools: [
+    {
+      description: 'Description of Tool 1',
+      name: 'Tool 1',
+    },
+    {
+      description: 'Description of Tool 2',
+      name: 'Tool 2',
+    },
+    {
+      description: 'Description of Tool 3',
+      name: 'Tool 3',
+    },
+    {
+      description: 'Description of Tool 4',
+      name: 'Tool 4',
+    },
+  ],
+};
+
+export const json2 = `
+{
+	"id": "chatcmpl-123",
+	"object": "chat.completion",
+	"created": 1677652288,
+	"model": "gpt-3.5-turbo-0613",
+	"system_fingerprint": "fp_44709d6fcb",
+	"choices": [{
+		"index": 0,
+		"message": {
+			"role": "assistant",
+			"content": "Hello there, how may I assist you today?"
+		},
+		"finish_reason": "stop"
+	}],
+	"usage": {
+		"prompt_tokens": 9,
+		"completion_tokens": 12,
+		"total_tokens": 21,
+    "debug_mode": true
+	}
+}
+`;

+ 26 - 0
playground/src/views/examples/json-viewer/index.vue

@@ -0,0 +1,26 @@
+<script lang="ts" setup>
+import { JsonViewer, Page } from '@vben/common-ui';
+
+import { Card } from 'ant-design-vue';
+
+import { json1, json2 } from './data';
+</script>
+<template>
+  <Page
+    title="Json Viewer"
+    description="一个渲染 JSON 结构数据的组件,支持复制、展开等,简单易用"
+  >
+    <Card title="默认配置">
+      <JsonViewer v-model="json1" />
+    </Card>
+    <Card title="可复制、默认展开3层、显示边框" class="mt-4">
+      <JsonViewer
+        v-model="json2"
+        :expand-depth="3"
+        copyable
+        :sort="false"
+        boxed
+      />
+    </Card>
+  </Page>
+</template>

+ 57 - 4
pnpm-lock.yaml

@@ -474,6 +474,9 @@ catalogs:
     vue-i18n:
       specifier: ^11.1.0
       version: 11.1.0
+    vue-json-viewer:
+      specifier: ^3.0.4
+      version: 3.0.4
     vue-router:
       specifier: ^4.5.0
       version: 4.5.0
@@ -1533,6 +1536,9 @@ importers:
       vue:
         specifier: ^3.5.13
         version: 3.5.13(typescript@5.7.3)
+      vue-json-viewer:
+        specifier: 'catalog:'
+        version: 3.0.4(vue@3.5.13(typescript@5.7.3))
       vue-router:
         specifier: 'catalog:'
         version: 4.5.0(vue@3.5.13(typescript@5.7.3))
@@ -3535,6 +3541,10 @@ packages:
     resolution: {integrity: sha512-DvpNSxiMrFqYMaGSRDDnQgO/L0MqNH4KWw9CUx8LRHHIdWp08En9DpmSRNpauUOxKpHAhyJJxx92BHZk9J84EQ==}
     engines: {node: '>= 16'}
 
+  '@intlify/shared@11.1.1':
+    resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==}
+    engines: {node: '>= 16'}
+
   '@intlify/unplugin-vue-i18n@6.0.3':
     resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==}
     engines: {node: '>= 18'}
@@ -5097,6 +5107,9 @@ packages:
     resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
     engines: {node: '>=18'}
 
+  clipboard@2.0.11:
+    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+
   clipboardy@4.0.0:
     resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==}
     engines: {node: '>=18'}
@@ -5589,6 +5602,9 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  delegate@3.2.0:
+    resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
+
   denque@2.1.0:
     resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
     engines: {node: '>=0.10'}
@@ -6451,6 +6467,9 @@ packages:
   globjoin@0.1.4:
     resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
 
+  good-listener@1.2.2:
+    resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
+
   gopd@1.2.0:
     resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
     engines: {node: '>= 0.4'}
@@ -8852,6 +8871,9 @@ packages:
   seemly@0.3.9:
     resolution: {integrity: sha512-bMLcaEqhIViiPbaumjLN8t1y+JpD/N8SiyYOyp0i0W6RgdyLWboIsUWAbZojF//JyerxPZR5Tgda+x3Pdne75A==}
 
+  select@1.1.2:
+    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
+
   semver-compare@1.0.0:
     resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
 
@@ -9381,6 +9403,9 @@ packages:
   through@2.3.8:
     resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
 
+  tiny-emitter@2.1.0:
+    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
+
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
@@ -9978,6 +10003,11 @@ packages:
     peerDependencies:
       vue: ^3.5.13
 
+  vue-json-viewer@3.0.4:
+    resolution: {integrity: sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==}
+    peerDependencies:
+      vue: ^3.5.13
+
   vue-router@4.5.0:
     resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
     peerDependencies:
@@ -12186,12 +12216,14 @@ snapshots:
 
   '@intlify/shared@11.1.0': {}
 
+  '@intlify/shared@11.1.1': {}
+
   '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.19.0(jiti@2.4.2))(rollup@4.34.2)(typescript@5.7.3)(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
       '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2))
       '@intlify/bundle-utils': 10.0.0(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))
-      '@intlify/shared': 11.1.0
-      '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
+      '@intlify/shared': 11.1.1
+      '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
       '@rollup/pluginutils': 5.1.4(rollup@4.34.2)
       '@typescript-eslint/scope-manager': 8.23.0
       '@typescript-eslint/typescript-estree': 8.23.0(typescript@5.7.3)
@@ -12213,11 +12245,11 @@ snapshots:
       - supports-color
       - typescript
 
-  '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.0)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
+  '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
     dependencies:
       '@babel/parser': 7.26.7
     optionalDependencies:
-      '@intlify/shared': 11.1.0
+      '@intlify/shared': 11.1.1
       '@vue/compiler-dom': 3.5.13
       vue: 3.5.13(typescript@5.7.3)
       vue-i18n: 11.1.0(vue@3.5.13(typescript@5.7.3))
@@ -14109,6 +14141,12 @@ snapshots:
       slice-ansi: 5.0.0
       string-width: 7.2.0
 
+  clipboard@2.0.11:
+    dependencies:
+      good-listener: 1.2.2
+      select: 1.1.2
+      tiny-emitter: 2.1.0
+
   clipboardy@4.0.0:
     dependencies:
       execa: 8.0.1
@@ -14610,6 +14648,8 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
+  delegate@3.2.0: {}
+
   denque@2.1.0: {}
 
   depcheck@1.4.7:
@@ -15675,6 +15715,10 @@ snapshots:
 
   globjoin@0.1.4: {}
 
+  good-listener@1.2.2:
+    dependencies:
+      delegate: 3.2.0
+
   gopd@1.2.0: {}
 
   graceful-fs@4.2.10: {}
@@ -18142,6 +18186,8 @@ snapshots:
 
   seemly@0.3.9: {}
 
+  select@1.1.2: {}
+
   semver-compare@1.0.0: {}
 
   semver@5.7.2: {}
@@ -18789,6 +18835,8 @@ snapshots:
 
   through@2.3.8: {}
 
+  tiny-emitter@2.1.0: {}
+
   tinybench@2.9.0: {}
 
   tinyexec@0.3.2: {}
@@ -19493,6 +19541,11 @@ snapshots:
       '@vue/devtools-api': 6.6.4
       vue: 3.5.13(typescript@5.7.3)
 
+  vue-json-viewer@3.0.4(vue@3.5.13(typescript@5.7.3)):
+    dependencies:
+      clipboard: 2.0.11
+      vue: 3.5.13(typescript@5.7.3)
+
   vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)):
     dependencies:
       '@vue/devtools-api': 6.6.4

+ 1 - 0
pnpm-workspace.yaml

@@ -175,6 +175,7 @@ catalog:
   vue: ^3.5.13
   vue-eslint-parser: ^9.4.3
   vue-i18n: ^11.1.0
+  vue-json-viewer: ^3.0.4
   vue-router: ^4.5.0
   vue-tippy: ^6.6.0
   vue-tsc: 2.1.10