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:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user