Bläddra i källkod

refactor: migrate json-viewer from vue-json-viewer to vue-json-pretty

- Replace vue-json-viewer with vue-json-pretty (actively maintained, Vue 3 native)
- Map original props to vue-json-pretty API in bindProps for backward compatibility
- Implement copy functionality via renderNodeActions slot with i18n support
- Update style.scss from .jv-* to .vjs-* class names
xingyu4j 3 veckor sedan
förälder
incheckning
f71094e878

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

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

+ 58 - 46
packages/effects/common-ui/src/components/json-viewer/index.vue

@@ -10,14 +10,12 @@ import type {
   JsonViewerValue,
 } from './types';
 
-import { computed, useAttrs } from 'vue';
-// @ts-expect-error - vue-json-viewer does not expose compatible typings for this import path
-import VueJsonViewerImport from 'vue-json-viewer';
+import { computed, ref, useAttrs } from 'vue';
+import VueJsonPretty from 'vue-json-pretty';
+import 'vue-json-pretty/lib/styles.css';
 
 import { $t } from '@vben/locales';
 
-import { isBoolean } from '@vben-core/shared/utils';
-
 import JsonBigint from 'json-bigint';
 
 defineOptions({ name: 'JsonViewer' });
@@ -42,33 +40,31 @@ const emit = defineEmits<{
   valueClick: [value: JsonViewerValue];
 }>();
 
-/** CJS/UMD 在 Vite 下解析为 { default: Component },需解包否则会出现 missing template or render */
-const VueJsonViewer =
-  (VueJsonViewerImport as { default?: typeof VueJsonViewerImport }).default ??
-  VueJsonViewerImport;
-
 const attrs: SetupContext['attrs'] = useAttrs();
 
-function handleClick(event: MouseEvent) {
-  if (
-    event.target instanceof HTMLElement &&
-    event.target.classList.contains('jv-item')
-  ) {
-    const pathNode = event.target.closest('.jv-push');
-    if (!pathNode || !pathNode.hasAttribute('path')) {
-      return;
+const copiedPath = ref<null | string>(null);
+
+const copyConfig = computed(() => {
+  return {
+    copiedText: $t('ui.jsonViewer.copied'),
+    copyText: $t('ui.jsonViewer.copy'),
+    timeout: 2000,
+  };
+});
+
+function handleCopy(node: any, defaultCopy: () => void) {
+  defaultCopy();
+  copiedPath.value = node.path;
+  emit('copied', {
+    action: 'copy',
+    text: JSON.stringify(node.content),
+    trigger: node.el ?? document.body,
+  });
+  setTimeout(() => {
+    if (copiedPath.value === node.path) {
+      copiedPath.value = null;
     }
-    const param: JsonViewerValue = {
-      el: event.target,
-      path: pathNode.getAttribute('path') || '',
-      depth: Number(pathNode.getAttribute('depth')) || 0,
-      value: event.target.textContent || undefined,
-    };
-
-    param.value = JSON.parse(param.value);
-    emit('valueClick', param);
-  }
-  emit('click', event);
+  }, copyConfig.value.timeout ?? 2000);
 }
 
 // 支持显示 bigint 数据,如较长的订单号
@@ -86,30 +82,46 @@ const jsonData = computed<Record<string, any>>(() => {
 });
 
 const bindProps = computed<Recordable<any>>(() => {
-  const copyable = {
-    copyText: $t('ui.jsonViewer.copy'),
-    copiedText: $t('ui.jsonViewer.copied'),
-    timeout: 2000,
-    ...(isBoolean(props.copyable) ? {} : props.copyable),
-  };
+  const prettyTheme =
+    props.theme === 'dark' || props.theme === 'dark-json-theme'
+      ? 'dark'
+      : 'light';
 
   return {
-    ...props,
     ...attrs,
-    value: jsonData.value,
-    onCopied: (event: JsonViewerAction) => emit('copied', event),
-    onKeyclick: (key: string) => emit('keyClick', key),
-    onClick: (event: MouseEvent) => handleClick(event),
-    copyable: props.copyable ? copyable : false,
+    data: jsonData.value,
+    deep: props.expanded ? Infinity : props.expandDepth,
+    showDoubleQuotes: props.showDoubleQuotes,
+    showLine: props.boxed,
+    showLength: true,
+    showIcon: true,
+    theme: prettyTheme,
+    collapsedNodeLength: props.previewMode ? 0 : Infinity,
+    renderNodeActions: !!props.copyable,
   };
 });
 </script>
 <template>
-  <VueJsonViewer v-bind="bindProps">
-    <template #copy="slotProps">
-      <slot name="copy" v-bind="slotProps"></slot>
-    </template>
-  </VueJsonViewer>
+  <div :class="[props.theme, { boxed: props.boxed }]" class="vben-json-viewer">
+    <VueJsonPretty v-bind="bindProps">
+      <template #renderNodeActions="{ node, defaultActions }">
+        <slot name="copy" :node="node" :default-actions="defaultActions">
+          <span
+            v-if="props.copyable"
+            class="vben-json-copy-btn"
+            :class="[{ 'is-copied': copiedPath === node.path }]"
+            @click.stop="handleCopy(node, defaultActions.copy)"
+          >
+            {{
+              copiedPath === node.path
+                ? copyConfig.copiedText
+                : copyConfig.copyText
+            }}
+          </span>
+        </slot>
+      </template>
+    </VueJsonPretty>
+  </div>
 </template>
 <style lang="scss">
 @use './style.scss';

+ 58 - 65
packages/effects/common-ui/src/components/json-viewer/style.scss

@@ -1,98 +1,91 @@
-.default-json-theme {
+.vben-json-viewer {
   font-family: Consolas, Menlo, Courier, monospace;
   font-size: 14px;
   color: hsl(var(--foreground));
-  white-space: nowrap;
   background: hsl(var(--background));
 
-  &.jv-container.boxed {
+  &.boxed {
+    padding: 10px;
     border: 1px solid hsl(var(--border));
+    border-radius: 4px;
   }
 
-  .jv-ellipsis {
-    display: inline-block;
-    padding: 0 4px 2px;
-    font-size: 0.9em;
-    line-height: 0.9;
-    vertical-align: 2px;
-    color: hsl(var(--secondary-foreground));
-    cursor: pointer;
-    user-select: none;
-    background-color: hsl(var(--secondary));
-    border-radius: 3px;
+  .vjs-key {
+    color: hsl(var(--heavy-foreground));
   }
 
-  .jv-button {
-    color: hsl(var(--primary));
+  .vjs-value-null,
+  .vjs-value-undefined {
+    color: hsl(var(--secondary-foreground));
   }
 
-  .jv-key {
-    color: hsl(var(--heavy-foreground));
+  .vjs-value-boolean {
+    color: hsl(var(--red-400));
   }
 
-  .jv-item {
-    &.jv-array {
-      color: hsl(var(--heavy-foreground));
-    }
-
-    &.jv-boolean {
-      color: hsl(var(--red-400));
-    }
+  .vjs-value-number {
+    color: hsl(var(--info-foreground));
+  }
 
-    &.jv-function {
-      color: hsl(var(--destructive-foreground));
-    }
+  .vjs-value-string {
+    color: hsl(var(--primary));
+    overflow-wrap: break-word;
+    white-space: normal;
+  }
 
-    &.jv-number {
-      color: hsl(var(--info-foreground));
-    }
+  .vjs-tree-brackets {
+    color: hsl(var(--heavy-foreground));
+  }
 
-    &.jv-number-float {
-      color: hsl(var(--info-foreground));
-    }
+  .vjs-comment {
+    color: hsl(var(--secondary-foreground));
+  }
 
-    &.jv-number-integer {
-      color: hsl(var(--info-foreground));
+  .vjs-tree-node {
+    &.is-highlight {
+      background-color: hsl(var(--accent));
     }
+  }
 
-    &.jv-object {
-      color: hsl(var(--accent-darker));
-    }
+  .vjs-carets {
+    cursor: pointer;
 
-    &.jv-undefined {
-      color: hsl(var(--secondary-foreground));
+    &:hover {
+      color: hsl(var(--primary));
     }
+  }
 
-    &.jv-string {
-      color: hsl(var(--primary));
-      overflow-wrap: break-word;
-      white-space: normal;
+  .vjs-indent-unit {
+    &.has-line {
+      border-left: 1px solid hsl(var(--border));
     }
   }
 
-  &.jv-container .jv-code {
-    padding: 10px;
+  .vben-json-copy-btn {
+    display: inline-block;
+    padding: 0 6px;
+    margin-left: 8px;
+    font-size: 12px;
+    line-height: 20px;
+    color: hsl(var(--primary));
+    cursor: pointer;
+    user-select: none;
+    background-color: hsl(var(--secondary));
+    border-radius: 3px;
+    opacity: 0;
+    transition: opacity 0.2s;
 
-    &.boxed:not(.open) {
-      padding-bottom: 20px;
-      margin-bottom: 10px;
+    &:hover {
+      background-color: hsl(var(--accent));
     }
 
-    &.open {
-      padding-bottom: 10px;
+    &.is-copied {
+      color: hsl(var(--success-foreground, var(--primary)));
+      opacity: 1;
     }
+  }
 
-    .jv-toggle {
-      &::before {
-        padding: 0 2px;
-        border-radius: 2px;
-      }
-
-      &:hover {
-        &::before {
-          background: hsl(var(--accent-foreground));
-        }
-      }
-    }
+  .vjs-tree-node:hover .vben-json-copy-btn {
+    opacity: 1;
   }
 }

+ 9 - 40
pnpm-lock.yaml

@@ -492,9 +492,9 @@ catalogs:
     vue-i18n:
       specifier: ^11.4.4
       version: 11.4.4
-    vue-json-viewer:
-      specifier: ^3.0.4
-      version: 3.0.4
+    vue-json-pretty:
+      specifier: ^2.6.0
+      version: 2.6.0
     vue-router:
       specifier: ^5.0.7
       version: 5.0.7
@@ -1689,9 +1689,9 @@ importers:
       vue:
         specifier: ^3.5.34
         version: 3.5.34(typescript@6.0.3)
-      vue-json-viewer:
+      vue-json-pretty:
         specifier: 'catalog:'
-        version: 3.0.4(vue@3.5.34(typescript@6.0.3))
+        version: 2.6.0(vue@3.5.34(typescript@6.0.3))
       vue-router:
         specifier: 'catalog:'
         version: 5.0.7(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
@@ -6404,9 +6404,6 @@ packages:
     resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==}
     engines: {node: '>=20'}
 
-  clipboard@2.0.11:
-    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
-
   cliui@6.0.0:
     resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
 
@@ -6805,9 +6802,6 @@ 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'}
@@ -7611,9 +7605,6 @@ packages:
   globrex@0.1.2:
     resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
 
-  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'}
@@ -9618,9 +9609,6 @@ packages:
   seemly@0.3.10:
     resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==}
 
-  select@1.1.2:
-    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
-
   semver-compare@1.0.0:
     resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
 
@@ -10085,9 +10073,6 @@ packages:
     resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
     engines: {node: '>=12.22'}
 
-  tiny-emitter@2.1.0:
-    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
-
   tinybench@2.9.0:
     resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
 
@@ -10757,8 +10742,9 @@ packages:
     peerDependencies:
       vue: ^3.5.34
 
-  vue-json-viewer@3.0.4:
-    resolution: {integrity: sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==}
+  vue-json-pretty@2.6.0:
+    resolution: {integrity: sha512-glz1aBVS35EO8+S9agIl3WOQaW2cJZW192UVKTuGmryx01ZvOVWc4pR3t+5UcyY4jdOfBUgVHjcpRpcnjRhCAg==}
+    engines: {node: '>= 10.0.0', npm: '>= 5.0.0'}
     peerDependencies:
       vue: ^3.5.34
 
@@ -15441,12 +15427,6 @@ snapshots:
       slice-ansi: 8.0.0
       string-width: 8.2.1
 
-  clipboard@2.0.11:
-    dependencies:
-      good-listener: 1.2.2
-      select: 1.1.2
-      tiny-emitter: 2.1.0
-
   cliui@6.0.0:
     dependencies:
       string-width: 4.2.3
@@ -15838,8 +15818,6 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
-  delegate@3.2.0: {}
-
   denque@2.1.0: {}
 
   depcheck@1.4.7:
@@ -16852,10 +16830,6 @@ snapshots:
 
   globrex@0.1.2: {}
 
-  good-listener@1.2.2:
-    dependencies:
-      delegate: 3.2.0
-
   gopd@1.2.0: {}
 
   graceful-fs@4.2.10: {}
@@ -18944,8 +18918,6 @@ snapshots:
 
   seemly@0.3.10: {}
 
-  select@1.1.2: {}
-
   semver-compare@1.0.0: {}
 
   semver@5.7.2:
@@ -19508,8 +19480,6 @@ snapshots:
 
   throttle-debounce@5.0.2: {}
 
-  tiny-emitter@2.1.0: {}
-
   tinybench@2.9.0: {}
 
   tinyclip@0.1.12: {}
@@ -20161,9 +20131,8 @@ snapshots:
       '@vue/devtools-api': 6.6.4
       vue: 3.5.34(typescript@6.0.3)
 
-  vue-json-viewer@3.0.4(vue@3.5.34(typescript@6.0.3)):
+  vue-json-pretty@2.6.0(vue@3.5.34(typescript@6.0.3)):
     dependencies:
-      clipboard: 2.0.11
       vue: 3.5.34(typescript@6.0.3)
 
   vue-router@5.0.7(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)):

+ 1 - 1
pnpm-workspace.yaml

@@ -203,7 +203,7 @@ catalog:
   vue: ^3.5.34
   vue-eslint-parser: ^10.4.0
   vue-i18n: ^11.4.4
-  vue-json-viewer: ^3.0.4
+  vue-json-pretty: ^2.6.0
   vue-router: ^5.0.7
   vue-tippy: ^6.7.1
   vue-tsc: ^3.3.1