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
This commit is contained in:
xingyu4j
2026-05-20 14:39:17 +08:00
parent 9badda3d11
commit f71094e878
5 changed files with 139 additions and 165 deletions

View File

@@ -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 param: JsonViewerValue = {
el: event.target,
path: pathNode.getAttribute('path') || '',
depth: Number(pathNode.getAttribute('depth')) || 0,
value: event.target.textContent || undefined,
};
const copiedPath = ref<null | string>(null);
param.value = JSON.parse(param.value);
emit('valueClick', param);
}
emit('click', event);
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;
}
}, 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';

View File

@@ -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;
.vjs-key {
color: hsl(var(--heavy-foreground));
}
.vjs-value-null,
.vjs-value-undefined {
color: hsl(var(--secondary-foreground));
}
.vjs-value-boolean {
color: hsl(var(--red-400));
}
.vjs-value-number {
color: hsl(var(--info-foreground));
}
.vjs-value-string {
color: hsl(var(--primary));
overflow-wrap: break-word;
white-space: normal;
}
.vjs-tree-brackets {
color: hsl(var(--heavy-foreground));
}
.vjs-comment {
color: hsl(var(--secondary-foreground));
}
.vjs-tree-node {
&.is-highlight {
background-color: hsl(var(--accent));
}
}
.vjs-carets {
cursor: pointer;
&:hover {
color: hsl(var(--primary));
}
}
.vjs-indent-unit {
&.has-line {
border-left: 1px solid hsl(var(--border));
}
}
.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;
.jv-button {
color: hsl(var(--primary));
}
.jv-key {
color: hsl(var(--heavy-foreground));
}
.jv-item {
&.jv-array {
color: hsl(var(--heavy-foreground));
&:hover {
background-color: hsl(var(--accent));
}
&.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));
overflow-wrap: break-word;
white-space: normal;
&.is-copied {
color: hsl(var(--success-foreground, var(--primary)));
opacity: 1;
}
}
&.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));
}
}
}
.vjs-tree-node:hover .vben-json-copy-btn {
opacity: 1;
}
}