Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ElAlert, ElCard, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { JsonEditor } from './index';
|
||||
|
||||
// 示例 1: 基础用法
|
||||
const basicJson = ref(
|
||||
'{\n "name": "John Doe",\n "age": 30,\n "email": "john@example.com",\n "active": true\n}',
|
||||
);
|
||||
const basicIsValid = ref(true);
|
||||
|
||||
// 示例 2: 对象初始化
|
||||
const objectJson = ref({
|
||||
id: 1,
|
||||
username: 'alice',
|
||||
roles: ['admin', 'user'],
|
||||
metadata: {
|
||||
lastLogin: '2025-11-13',
|
||||
loginCount: 42,
|
||||
},
|
||||
});
|
||||
|
||||
// 示例 3: 只读模式
|
||||
const readOnlyJson = ref({
|
||||
status: 'success',
|
||||
code: 200,
|
||||
message: 'Operation completed successfully',
|
||||
data: {
|
||||
id: 123,
|
||||
title: 'Example Data',
|
||||
},
|
||||
});
|
||||
|
||||
// 示例 4: 配置编辑
|
||||
const configJson = ref({
|
||||
theme: 'dark',
|
||||
language: 'zh-CN',
|
||||
notifications: {
|
||||
enabled: true,
|
||||
sound: false,
|
||||
desktop: true,
|
||||
},
|
||||
privacy: {
|
||||
profilePublic: false,
|
||||
showOnlineStatus: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 事件处理
|
||||
function handleValid() {
|
||||
basicIsValid.value = true;
|
||||
}
|
||||
|
||||
function handleInvalid(error: string) {
|
||||
basicIsValid.value = false;
|
||||
console.error('JSON 无效:', error);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="json-editor-example">
|
||||
<h1>JSON Editor 组件示例</h1>
|
||||
|
||||
<ElTabs>
|
||||
<!-- 示例 1: 基础用法 -->
|
||||
<ElTabPane label="基础用法">
|
||||
<ElCard class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>简单的 JSON 编辑器</span>
|
||||
<span class="status" :class="{ valid: basicIsValid }">
|
||||
{{ basicIsValid ? '✓ 有效' : '✗ 无效' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<JsonEditor
|
||||
v-model="basicJson"
|
||||
placeholder="输入或粘贴 JSON 内容"
|
||||
@valid="handleValid"
|
||||
@invalid="handleInvalid"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 示例 2: 对象初始化 -->
|
||||
<ElTabPane label="对象初始化">
|
||||
<ElCard class="box-card">
|
||||
<template #header>
|
||||
<span>直接传入对象进行初始化</span>
|
||||
</template>
|
||||
|
||||
<p class="description">
|
||||
直接传入 JavaScript 对象,组件会自动转换为 JSON
|
||||
字符串,并以指定的缩进格式显示。
|
||||
</p>
|
||||
|
||||
<JsonEditor
|
||||
:model-value="objectJson"
|
||||
:indent="2"
|
||||
@change="(json) => console.log('Changed:', json)"
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 示例 3: 只读模式 -->
|
||||
<ElTabPane label="只读模式">
|
||||
<ElCard class="box-card">
|
||||
<template #header>
|
||||
<span>只读模式 - 用于展示 API 响应</span>
|
||||
</template>
|
||||
|
||||
<p class="description">
|
||||
设置 readonly 属性为 true,可以创建只读的 JSON 查看器,适合展示 API
|
||||
响应或配置信息。
|
||||
</p>
|
||||
|
||||
<JsonEditor
|
||||
:model-value="readOnlyJson"
|
||||
readonly
|
||||
:show-format-button="false"
|
||||
line-numbers
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 示例 4: 配置编辑 -->
|
||||
<ElTabPane label="配置编辑">
|
||||
<ElCard class="box-card">
|
||||
<template #header>
|
||||
<span>应用配置编辑</span>
|
||||
</template>
|
||||
|
||||
<p class="description">
|
||||
用于编辑应用配置文件。支持格式化、压缩等功能。
|
||||
</p>
|
||||
|
||||
<JsonEditor
|
||||
:model-value="configJson"
|
||||
:min-height="300"
|
||||
:max-height="500"
|
||||
placeholder="编辑配置信息"
|
||||
highlight-syntax
|
||||
/>
|
||||
</ElCard>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- 示例 5: 功能介绍 -->
|
||||
<ElTabPane label="功能介绍">
|
||||
<div class="features">
|
||||
<h3>🎯 主要功能</h3>
|
||||
|
||||
<div class="feature-group">
|
||||
<h4>✨ 编辑功能</h4>
|
||||
<ul>
|
||||
<li>📝 实时 JSON 编辑</li>
|
||||
<li>🔍 JSON 验证与错误提示</li>
|
||||
<li>🎨 语法高亮显示</li>
|
||||
<li>📊 行号显示</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<h4>🛠️ 工具功能</h4>
|
||||
<ul>
|
||||
<li>✂️ 格式化 JSON (Ctrl+Shift+F)</li>
|
||||
<li>📦 压缩 JSON</li>
|
||||
<li>📋 复制到剪贴板</li>
|
||||
<li>🗑️ 清空内容</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<h4>⚙️ 配置选项</h4>
|
||||
<ul>
|
||||
<li>🔒 只读模式</li>
|
||||
<li>🚫 禁用编辑</li>
|
||||
<li>📏 自定义高度</li>
|
||||
<li>🎛️ 缩进控制</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<h4>📡 事件系统</h4>
|
||||
<ul>
|
||||
<li>✅ valid - JSON 有效时触发</li>
|
||||
<li>❌ invalid - JSON 无效时触发</li>
|
||||
<li>🔄 change - 内容变化时触发</li>
|
||||
<li>📤 update:modelValue - v-model 更新</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
title="快捷键"
|
||||
type="info"
|
||||
:closable="false"
|
||||
description="按下 Ctrl+Shift+F (Windows/Linux) 或 Cmd+Shift+F (Mac) 快速格式化 JSON"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.json-editor-example {
|
||||
max-width: 1200px;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.status {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--destructive));
|
||||
background-color: hsl(var(--destructive) / 10%);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.valid {
|
||||
color: hsl(var(--primary));
|
||||
background-color: hsl(var(--primary) / 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.features {
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.feature-group {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--foreground));
|
||||
|
||||
&::before {
|
||||
margin-right: 8px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as JsonEditor } from './json-editor.vue';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,820 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
JsonEditorEmits,
|
||||
JsonEditorProps,
|
||||
JsonValidationResult,
|
||||
} from './types';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElButton, ElMessage, ElTooltip } from 'element-plus';
|
||||
|
||||
defineOptions({
|
||||
name: 'JsonEditor',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
mode: 'simple',
|
||||
indent: 4,
|
||||
placeholder: () => '输入或粘贴 JSON',
|
||||
lineNumbers: true,
|
||||
minHeight: 400,
|
||||
maxHeight: 500,
|
||||
autoFormat: true,
|
||||
highlightSyntax: true,
|
||||
showFormatButton: true,
|
||||
showValidation: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<JsonEditorEmits>();
|
||||
|
||||
interface Props extends JsonEditorProps {}
|
||||
|
||||
// 编辑器内容
|
||||
const content = ref('');
|
||||
const textareaRef = ref<HTMLTextAreaElement>();
|
||||
const lineNumbersRef = ref<HTMLDivElement>();
|
||||
|
||||
// 验证结果
|
||||
const validationResult = ref<JsonValidationResult>({ valid: true });
|
||||
const isFormatting = ref(false);
|
||||
|
||||
// 初始化内容
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
const initialValue =
|
||||
typeof props.modelValue === 'string'
|
||||
? props.modelValue
|
||||
: JSON.stringify(props.modelValue, null, props.indent);
|
||||
content.value = initialValue;
|
||||
validateJson(initialValue);
|
||||
}
|
||||
syncLineNumbers();
|
||||
});
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue !== undefined) {
|
||||
const stringValue =
|
||||
typeof newValue === 'string'
|
||||
? newValue
|
||||
: JSON.stringify(newValue, null, props.indent);
|
||||
if (stringValue !== content.value) {
|
||||
content.value = stringValue;
|
||||
validateJson(stringValue);
|
||||
syncLineNumbers();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 验证 JSON 的有效性
|
||||
*/
|
||||
function validateJson(jsonString: string): JsonValidationResult {
|
||||
try {
|
||||
if (!jsonString.trim()) {
|
||||
validationResult.value = { valid: true };
|
||||
return { valid: true };
|
||||
}
|
||||
JSON.parse(jsonString);
|
||||
validationResult.value = { valid: true };
|
||||
emit('valid');
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '格式错误';
|
||||
validationResult.value = { valid: false, error: errorMessage };
|
||||
emit('invalid', errorMessage);
|
||||
return { valid: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 JSON
|
||||
*/
|
||||
function formatJson() {
|
||||
if (!content.value.trim()) {
|
||||
ElMessage.warning($t('common.jsonEditor.emptyContent'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isFormatting.value = true;
|
||||
const parsed = JSON.parse(content.value);
|
||||
const formatted = JSON.stringify(parsed, null, props.indent);
|
||||
content.value = formatted;
|
||||
validateJson(formatted);
|
||||
syncLineNumbers();
|
||||
ElMessage.success($t('common.jsonEditor.formatSuccess'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: $t('common.jsonEditor.invalidJson');
|
||||
ElMessage.error($t('common.jsonEditor.formatFailed', [errorMessage]));
|
||||
} finally {
|
||||
isFormatting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩 JSON
|
||||
*/
|
||||
function compressJson() {
|
||||
if (!content.value.trim()) {
|
||||
ElMessage.warning($t('common.jsonEditor.emptyContent'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(content.value);
|
||||
const compressed = JSON.stringify(parsed);
|
||||
content.value = compressed;
|
||||
validateJson(compressed);
|
||||
syncLineNumbers();
|
||||
ElMessage.success($t('common.jsonEditor.compressSuccess'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: $t('common.jsonEditor.invalidJson');
|
||||
ElMessage.error($t('common.jsonEditor.compressFailed', [errorMessage]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制到剪贴板
|
||||
*/
|
||||
function copyToClipboard() {
|
||||
if (!content.value) {
|
||||
ElMessage.warning($t('common.jsonEditor.noContent'));
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(content.value)
|
||||
.then(() => {
|
||||
ElMessage.success($t('common.jsonEditor.copiedSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error($t('common.jsonEditor.copyFailed'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空内容
|
||||
*/
|
||||
function clearContent() {
|
||||
content.value = '';
|
||||
validationResult.value = { valid: true };
|
||||
emit('update:modelValue', '');
|
||||
emit('change', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步行号
|
||||
*/
|
||||
function syncLineNumbers() {
|
||||
if (!lineNumbersRef.value || !textareaRef.value) return;
|
||||
|
||||
const lines = content.value.split('\n').length;
|
||||
const lineNumbersHtml = Array.from({ length: lines }, (_, i) => i + 1).join(
|
||||
'<br>',
|
||||
);
|
||||
lineNumbersRef.value.innerHTML = lineNumbersHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理输入事件
|
||||
*/
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
content.value = target.value;
|
||||
syncLineNumbers();
|
||||
validateJson(content.value);
|
||||
emit('update:modelValue', content.value);
|
||||
emit('change', content.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚动事件
|
||||
*/
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLTextAreaElement;
|
||||
if (lineNumbersRef.value) {
|
||||
lineNumbersRef.value.scrollTop = target.scrollTop;
|
||||
lineNumbersRef.value.scrollLeft = target.scrollLeft;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按键事件
|
||||
*/
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Ctrl/Cmd + Shift + F 格式化
|
||||
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'F') {
|
||||
event.preventDefault();
|
||||
formatJson();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行号容器样式
|
||||
*/
|
||||
const lineNumbersStyle = computed(() => ({
|
||||
minHeight: `${props.minHeight}px`,
|
||||
maxHeight: `${props.maxHeight}px`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 获取编辑器容器样式
|
||||
*/
|
||||
const editorStyle = computed(() => ({
|
||||
minHeight: `${props.minHeight}px`,
|
||||
maxHeight: `${props.maxHeight}px`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 获取验证状态类
|
||||
*/
|
||||
const validationClass = computed(() => {
|
||||
if (!props.showValidation) return '';
|
||||
return validationResult.value.valid ? 'is-valid' : 'is-invalid';
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取验证错误信息
|
||||
*/
|
||||
const errorMessage = computed(() => {
|
||||
return validationResult.value.error || '';
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
formatJson,
|
||||
compressJson,
|
||||
copyToClipboard,
|
||||
clearContent,
|
||||
validateJson,
|
||||
getContent: () => content.value,
|
||||
getParsedJson: () => JSON.parse(content.value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* JSON 语法高亮函数
|
||||
*/
|
||||
function highlightJsonSyntax(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
return (
|
||||
text
|
||||
// 转义 HTML 特殊字符
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
// 高亮字符串
|
||||
.replaceAll(
|
||||
/("(?:\\.|[^"\\])*"(?=\s*[,:}\]]))/g,
|
||||
'<span class="json-string">$1</span>',
|
||||
)
|
||||
// 高亮数字
|
||||
.replaceAll(
|
||||
/(?<=[:[\s,])(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:e[+-]?\d+)?)\b/gi,
|
||||
'<span class="json-number">$1</span>',
|
||||
)
|
||||
// 高亮布尔值
|
||||
.replaceAll(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
|
||||
// 高亮 null
|
||||
.replaceAll(/\bnull\b/g, '<span class="json-null">$1</span>')
|
||||
// 高亮冒号和逗号
|
||||
.replaceAll(/(:)/g, '<span class="json-colon">$1</span>')
|
||||
.replaceAll(/(,)/g, '<span class="json-comma">$1</span>')
|
||||
// 高亮括号
|
||||
.replaceAll(/([{}[\]])/g, '<span class="json-bracket">$1</span>')
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 简洁模式 -->
|
||||
<div
|
||||
v-if="mode === 'simple'"
|
||||
class="json-editor-simple"
|
||||
:class="{ 'is-disabled': disabled, 'is-readonly': readonly }"
|
||||
>
|
||||
<div class="json-editor-simple-wrapper">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="json-textarea-simple"
|
||||
:value="content"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
spellcheck="false"
|
||||
@input="handleInput"
|
||||
@keydown="handleKeydown"
|
||||
></textarea>
|
||||
<pre
|
||||
v-if="highlightSyntax"
|
||||
class="json-highlight-simple"
|
||||
aria-hidden="true"
|
||||
><code v-html="highlightJsonSyntax(content)"></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整模式 -->
|
||||
<div
|
||||
v-else
|
||||
class="json-editor"
|
||||
:class="[
|
||||
validationClass,
|
||||
{ 'is-readonly': readonly, 'is-disabled': disabled },
|
||||
]"
|
||||
>
|
||||
<!-- 工具栏 -->
|
||||
<div class="json-editor-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span
|
||||
v-if="showValidation"
|
||||
class="status-indicator"
|
||||
:class="{ 'is-valid': validationResult.valid }"
|
||||
>
|
||||
{{
|
||||
validationResult.valid
|
||||
? $t('common.jsonEditor.valid')
|
||||
: $t('common.jsonEditor.invalid')
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
v-if="!validationResult.valid && errorMessage"
|
||||
class="error-message"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<ElTooltip
|
||||
:content="`${$t('common.jsonEditor.format')} (Ctrl+Shift+F)`"
|
||||
>
|
||||
<ElButton
|
||||
v-if="showFormatButton"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="disabled || readonly || isFormatting"
|
||||
@click="formatJson"
|
||||
>
|
||||
{{ $t('common.jsonEditor.format') }}
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
|
||||
<ElTooltip :content="$t('common.jsonEditor.compress')">
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="disabled || readonly"
|
||||
@click="compressJson"
|
||||
>
|
||||
{{ $t('common.jsonEditor.compress') }}
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
|
||||
<ElTooltip :content="$t('common.jsonEditor.copy')">
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="copyToClipboard"
|
||||
>
|
||||
{{ $t('common.jsonEditor.copy') }}
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
|
||||
<ElTooltip :content="$t('common.jsonEditor.clear')">
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="disabled || readonly"
|
||||
@click="clearContent"
|
||||
>
|
||||
{{ $t('common.jsonEditor.clear') }}
|
||||
</ElButton>
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器容器 -->
|
||||
<div class="json-editor-container" :style="editorStyle">
|
||||
<!-- 行号 -->
|
||||
<div
|
||||
v-if="lineNumbers"
|
||||
ref="lineNumbersRef"
|
||||
class="line-numbers"
|
||||
:style="lineNumbersStyle"
|
||||
></div>
|
||||
|
||||
<!-- 文本区域 -->
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
class="json-textarea"
|
||||
:value="content"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:style="editorStyle"
|
||||
spellcheck="false"
|
||||
@input="handleInput"
|
||||
@scroll="handleScroll"
|
||||
@keydown="handleKeydown"
|
||||
></textarea>
|
||||
|
||||
<!-- 语法高亮层(仅用于视觉效果) -->
|
||||
<pre
|
||||
v-if="highlightSyntax"
|
||||
class="json-highlight"
|
||||
:style="editorStyle"
|
||||
aria-hidden="true"
|
||||
><code v-html="highlightJsonSyntax(content)"></code></pre>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div v-if="showValidation" class="json-editor-footer">
|
||||
<span class="stats">
|
||||
{{
|
||||
$t('common.jsonEditor.stats', [
|
||||
content.split('\n').length,
|
||||
content.length,
|
||||
])
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.json-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-color: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.is-invalid {
|
||||
border-color: hsl(var(--destructive));
|
||||
|
||||
.json-textarea,
|
||||
.json-highlight {
|
||||
border-color: hsl(var(--destructive));
|
||||
}
|
||||
}
|
||||
|
||||
&.is-valid {
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
&.is-readonly {
|
||||
background-color: hsl(var(--background-deep) / 50%);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
|
||||
.json-textarea {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.json-editor-toolbar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
hsl(var(--background)) 0%,
|
||||
hsl(var(--background-deep) / 30%) 100%
|
||||
);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-button) {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
white-space: nowrap;
|
||||
background-color: hsl(var(--background-deep));
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.is-valid {
|
||||
color: hsl(var(--primary));
|
||||
background-color: hsl(var(--primary) / 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--destructive));
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.json-editor-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 50px;
|
||||
padding: 8px 12px;
|
||||
overflow: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
background-color: hsl(var(--background-deep) / 50%);
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
|
||||
br {
|
||||
display: block;
|
||||
height: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.json-textarea {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
word-wrap: break-word;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
caret-color: hsl(var(--primary));
|
||||
resize: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 50%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.json-highlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: transparent;
|
||||
word-wrap: break-word;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
pointer-events: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
code {
|
||||
display: block;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
:deep(.json-string) {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
:deep(.json-number) {
|
||||
color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
:deep(.json-boolean) {
|
||||
color: hsl(var(--chart-2));
|
||||
}
|
||||
|
||||
:deep(.json-null) {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
:deep(.json-colon) {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
:deep(.json-comma) {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
:deep(.json-bracket) {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
.json-editor-footer {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 16px;
|
||||
background-color: hsl(var(--background-deep) / 30%);
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
|
||||
.stats {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= 简洁模式样式 ============= */
|
||||
.json-editor-simple {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: hsl(var(--primary) / 50%);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 10%);
|
||||
}
|
||||
|
||||
&.is-readonly {
|
||||
cursor: default;
|
||||
background-color: hsl(var(--background-deep) / 50%);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: hsl(var(--background-deep));
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.json-editor-simple-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.json-textarea-simple {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--foreground));
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
tab-size: 2;
|
||||
white-space: pre-wrap;
|
||||
caret-color: hsl(var(--primary));
|
||||
resize: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 50%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.json-highlight-simple {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: transparent;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
tab-size: 2;
|
||||
white-space: pre-wrap;
|
||||
pointer-events: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
code {
|
||||
display: inline;
|
||||
color: transparent;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
:deep(.json-string) {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
:deep(.json-number) {
|
||||
color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
:deep(.json-boolean) {
|
||||
color: hsl(var(--chart-2));
|
||||
}
|
||||
|
||||
:deep(.json-null) {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
:deep(.json-colon) {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
:deep(.json-comma) {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
:deep(.json-bracket) {
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* JSON Editor Props and Types
|
||||
*/
|
||||
export interface JsonEditorProps {
|
||||
/**
|
||||
* JSON 字符串或对象
|
||||
*/
|
||||
modelValue?: Record<string, any> | string;
|
||||
|
||||
/**
|
||||
* 是否只读
|
||||
* @default false
|
||||
*/
|
||||
readonly?: boolean;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器模式: 'full' 或 'simple'
|
||||
* - full: 完整功能的 JSON 编辑器(有工具栏、行号、页脚等)
|
||||
* - simple: 简洁模式,外观类似 ElInput,仅有彩色高亮
|
||||
* @default 'full'
|
||||
*/
|
||||
mode?: 'full' | 'simple';
|
||||
|
||||
/**
|
||||
* 缩进空格数
|
||||
* @default 2
|
||||
*/
|
||||
indent?: number;
|
||||
|
||||
/**
|
||||
* 占位符文本
|
||||
* @default 使用 i18n 的 'jsonEditor.placeholder'
|
||||
*/
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* 是否启用行号
|
||||
* @default true
|
||||
*/
|
||||
lineNumbers?: boolean;
|
||||
|
||||
/**
|
||||
* 最小高度(像素)
|
||||
* @default 200
|
||||
*/
|
||||
minHeight?: number;
|
||||
|
||||
/**
|
||||
* 最大高度(像素)
|
||||
* @default 600
|
||||
*/
|
||||
maxHeight?: number;
|
||||
|
||||
/**
|
||||
* 是否启用自动格式化
|
||||
* @default true
|
||||
*/
|
||||
autoFormat?: boolean;
|
||||
|
||||
/**
|
||||
* 是否启用语法高亮
|
||||
* @default true
|
||||
*/
|
||||
highlightSyntax?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示格式化按钮
|
||||
* @default true
|
||||
*/
|
||||
showFormatButton?: boolean;
|
||||
|
||||
/**
|
||||
* 是否显示校验结果
|
||||
* @default true
|
||||
*/
|
||||
showValidation?: boolean;
|
||||
}
|
||||
|
||||
export interface JsonEditorEmits {
|
||||
/**
|
||||
* 当内容变化时触发
|
||||
*/
|
||||
'update:modelValue': [value: string];
|
||||
|
||||
/**
|
||||
* 当内容变化时触发
|
||||
*/
|
||||
change: [value: string];
|
||||
|
||||
/**
|
||||
* 当 JSON 有效时触发
|
||||
*/
|
||||
valid: [];
|
||||
|
||||
/**
|
||||
* 当 JSON 无效时触发
|
||||
*/
|
||||
invalid: [error: string];
|
||||
}
|
||||
|
||||
export interface JsonValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user