一、前言
最近做了一个需求,要求实现可以对json结构进行编辑保存及字段级别的权限管控,结合目前已有的轮子和当前项目的结构,决定使用vue-json-pretty和vue-json-schema-editor-visual来实现
效果如下
组件支持修改左侧json数据,自动生成右侧树。也支持修改右侧树,生成左侧json数据,可以通过勾选复选框来实现实际的返回数据的管控。功能较为简单,将高级配置隐藏了,如需要定制化可以自己改一下代码使用。详细的数据结构参考文章
二、详细代码
1. 安装依赖
npm install vue-json-schema-editor-visual
import JsonSchemaEditor from 'vue-json-schema-editor-visual';
Vue.use(JsonSchemaEditor);
Tip:由于我的项目是vue2项目,此处安装v1版本即可,vue3项目则不需要加上v1,详细可参考官网
npm install vue-json-pretty@v1-latest
在main.js引入 或者在文件内引入
import VueJsonPretty from 'vue-json-pretty';
import 'vue-json-pretty/lib/styles.css';
// main.js引入
Vue.component("vue-json-pretty", VueJsonPretty);
// .vue文件引入
export default {
components: {
VueJsonPretty,
},
};
2.封装组件
封装的组件 JsonEditorContainer.vue
<template>
<div class="json-editor-container">
<vue-json-editor
ref="jsonEditor"
v-model="localJsonData"
:expanded-on-start="true"
:mode="jsonModel"
:showBtns="false"
class="code-json-editor"
lang="zh"
@json-change="handleJsonChange"
@has-error="handleError"
></vue-json-editor>
<s-json-schema-editor
v-if="!isDisabled"
:key="localReload"
class="json-schema-editor"
ref="schema"
show-default-value
:schema.sync="localJsonTree"
/>
</div>
</template>
<script>
import debounce from "lodash/debounce";
/**
* JsonEditorContainer 组件
* 用于在左侧编辑 JSON 数据,右侧显示 JSON Schema 表单,并支持双向数据绑定。
* 支持 JSON 数据的编辑、校验、转换,以及 JSON Schema 的生成和编辑。
*/
export default {
name: "JsonEditorContainer",
props: {
value: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
configField: {
type: String,
default: "", // 默认值为空对象的 JSON 字符串
},
},
data() {
return {
localJsonData: this.safeJSONParse(this.value),
localJsonTree: this.validatenull(this.configField)
? this.convertToTree(this.value)
: this.safeJSONParse(this.configField),
localConfigField: this.safeJSONParse(this.configField), // 解析 configField
localReload: null,
mutationObserver: null,
hasJsonFlag: true,
isEditingJsonData: false,
};
},
computed: {
jsonModel() {
return this.isDisabled ? "view" : "code";
},
isDisabled() {
return this.disabled;
},
},
watch: {
localJsonData: {
handler: debounce(function (newVal) {
this.$emit(this.hasJsonFlag ? "json-change" : "has-error", newVal);
if (this.isEditingJsonData && this.hasJsonFlag && !this.isDisabled) {
this.$emit("input", JSON.stringify(newVal));
this.localJsonTree = this.convertToTree(newVal);
this.$emit("update:configField", JSON.stringify(this.localJsonTree));
this.localReload = Math.random(); // 更新 reload
}
}, 500),
deep: true,
},
localJsonTree: {
handler: debounce(function (newVal) {
if (!this.isEditingJsonData && !this.isDisabled) {
this.localJsonData = this.treeToJson(newVal);
this.$emit("input", JSON.stringify(this.localJsonData));
this.$emit("update:configField", JSON.stringify(newVal));
}
}, 500),
deep: true,
},
},
beforeDestroy() {
this.stopObservingEditor();
},
mounted() {
if (!this.isDisabled) {
this.startObservingEditor();
}
},
methods: {
// 左侧json编辑框改变时触发
handleJsonChange() {
if (this.isDisabled) return;
this.hasJsonFlag = true;
},
// 左侧json编辑框错误时触发
handleError() {
if (this.isDisabled) return;
this.hasJsonFlag = false;
},
// 检查json是否合法
checkJson() {
if (this.hasJsonFlag === false) {
return false;
} else {
return true;
}
},
// 将json字符串转换为json对象
safeJSONParse(jsonData) {
if (typeof jsonData === "string") {
try {
return JSON.parse(jsonData);
} catch (error) {
// console.error("Invalid JSON string:", error);
return null; // 解析失败返回null或根据需要返回其他默认值
}
} else if (typeof jsonData === "object" && jsonData !== null) {
return jsonData; // 如果是对象,直接返回
} else {
// console.error("Input is neither a string nor an object.");
return null; // 输入既不是字符串也不是对象时,返回null或其他默认值
}
},
// 将json转换为json schema
convertToTree(jsonData) {
function getType(value) {
if (value === null) return "object";
if (Array.isArray(value)) return "array";
if (typeof value === "number") {
return Number.isInteger(value) ? "integer" : "number";
}
return typeof value;
}
function buildTree(data, title = "root") {
const type = getType(data);
const tree = {
type: type,
title: title,
};
if (type === "object") {
tree.properties = {};
const currentRequiredFields = Object.keys(data);
for (const key in data) {
tree.properties[key] = buildTree(data[key], key);
}
if (currentRequiredFields.length > 0) {
tree.required = currentRequiredFields;
}
} else if (type === "array") {
if (data.length > 0) {
tree.items = buildTree(data[0], "items");
} else {
tree.items = { type: "object" };
}
} else {
tree.default = data;
}
return tree;
}
return buildTree(this.safeJSONParse(jsonData));
},
// 将json schema转换为json
treeToJson(tree) {
function buildJson(treeNode) {
let result;
if (treeNode.type === "object") {
result = {};
const requiredFields =
treeNode.required || Object.keys(treeNode.properties || {});
if (treeNode.properties) {
for (const key of requiredFields) {
if (treeNode.properties[key]) {
result[key] = buildJson(treeNode.properties[key]);
}
}
}
} else if (treeNode.type === "array") {
result = [];
if (treeNode.items) {
result.push(buildJson(treeNode.items));
}
} else if (treeNode.type === "integer" || treeNode.type === "number") {
// 对于 "integer" 和 "number",执行 Number 方法
result = Number(
treeNode.default !== undefined ? treeNode.default : null
);
} else if (treeNode.type === "boolean") {
// 处理 boolean 类型,严格判断字符串 "true" 或 "false"
if (treeNode.default === "true") {
result = true;
} else if (treeNode.default === "false") {
result = false;
} else {
result = Boolean(treeNode.default); // 处理非字符串的布尔值
}
} else {
result = treeNode.default !== undefined ? treeNode.default : null;
}
return result;
}
return buildJson(tree);
},
// 监听左侧编辑器是否处于编辑状态
startObservingEditor() {
const editorElement = this.$refs.jsonEditor.$el;
const jsoneditor = editorElement.querySelector(".ace-jsoneditor");
// 初始化 MutationObserver
this.mutationObserver = new MutationObserver(
debounce((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class"
) {
// 判断元素是否同时包含 ace-jsoneditor 和 ace-focus 类名
const hasFocus = jsoneditor.classList.contains("ace_focus");
this.$set(this, "isEditingJsonData", hasFocus);
}
});
}, 200) // 去抖动时间间隔(毫秒)
);
// 配置观察器以监控类名的变化
this.mutationObserver.observe(jsoneditor, {
attributes: true,
subtree: false, // 只监控当前元素,不监控子元素
attributeFilter: ["class"],
});
},
// 停止监听
stopObservingEditor() {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
},
},
};
</script>
<style lang="scss" scoped>
.json-editor-container {
display: flex;
}
:deep(.json-schema-editor) {
width: 55%;
height: 70vh;
overflow: auto;
margin-left: 10px;
.col-item-type {
width: 18%;
margin-left: 8px;
}
.col-item-desc,
.adv-set {
display: none;
}
.col-item-mock {
margin-left: 8px;
.el-input__inner {
border-radius: 4px;
}
.el-input-group__append {
display: none;
}
}
.col-item-type,
.col-item-mock,
.col-item-setting {
margin-top: -6px;
}
}
.code-json-editor {
flex: 1;
::v-deep {
.jsoneditor-poweredBy {
display: none !important;
}
div.jsoneditor-outer {
height: 70vh;
}
div.jsoneditor-menu {
background-color: #000;
border-bottom: 1px solid #000;
}
div.jsoneditor {
border: 1px solid #000;
}
}
}
</style>
3.使用方法
<el-form-item>
<template #jsonResultForm="{ disabled }">
<JsonEditorContainer
class="form-json-editor"
v-model="fieldForm.jsonResult"
:disabled="disabled"
:configField.sync="fieldForm.configField"
@json-change="onJsonChange"
@has-error="onError"
/>
</template>
</el-form-item>
v-model绑定的为左侧的json数据,configField为右侧的json结构树,数据格式都为json字符串(JSON.stringify()的数据),类似以下数据
json数据
{\"code\":0,\"data\":{\"current\":0,\"hitCount\":true,\"pages\":0,\"records\":[{\"api\":\"接口\",\"apiName\":\"接口名称\",\"appCode\":\"应用code\",\"appName\":\"应用名称\",\"fieldTemplateId\":\"模板名称\",\"jsonTemplate\":\"接口结果模板\"}],\"searchCount\":true,\"size\":0,\"total\":0},\"msg\":\"操作成功\",\"success\":true}
格式化后
{
"code": 0,
"data": {
"current": 0,
"hitCount": true,
"pages": 0,
"records": [
{
"api": "接口",
"apiName": "接口名称",
"appCode": "应用code",
"appName": "应用名称",
"fieldTemplateId": "模板名称",
"jsonTemplate": "接口结果模板"
}
],
"searchCount": true,
"size": 0,
"total": 0
},
"msg": "操作成功",
"success": true
}
configField数据
{\"type\":\"object\",\"title\":\"root\",\"properties\":{\"code\":{\"type\":\"integer\",\"title\":\"code\",\"default\":0},\"data\":{\"type\":\"object\",\"title\":\"data\",\"properties\":{\"current\":{\"type\":\"integer\",\"title\":\"current\",\"default\":0},\"hitCount\":{\"type\":\"boolean\",\"title\":\"hitCount\",\"default\":true},\"pages\":{\"type\":\"integer\",\"title\":\"pages\",\"default\":0},\"records\":{\"type\":\"array\",\"title\":\"records\",\"items\":{\"type\":\"object\",\"title\":\"items\",\"properties\":{\"api\":{\"type\":\"string\",\"title\":\"api\",\"default\":\"接口\"},\"apiName\":{\"type\":\"string\",\"title\":\"apiName\",\"default\":\"接口名称\"},\"appCode\":{\"type\":\"string\",\"title\":\"appCode\",\"default\":\"应用code\"},\"appName\":{\"type\":\"string\",\"title\":\"appName\",\"default\":\"应用名称\"},\"fieldTemplateId\":{\"type\":\"string\",\"title\":\"fieldTemplateId\",\"default\":\"模板名称\"},\"jsonTemplate\":{\"type\":\"string\",\"title\":\"jsonTemplate\",\"default\":\"接口结果模板\"},\"ruleConditionId\":{\"type\":\"string\",\"title\":\"ruleConditionId\",\"default\":\"ShenYu的选择器规则ID\"},\"selectorId\":{\"type\":\"string\",\"title\":\"selectorId\",\"default\":\"ShenYu的选择器Id\"}},\"required\":[\"api\",\"apiName\",\"appCode\",\"appName\",\"fieldTemplateId\",\"jsonTemplate\"]}},\"searchCount\":{\"type\":\"boolean\",\"title\":\"searchCount\",\"default\":true},\"size\":{\"type\":\"integer\",\"title\":\"size\",\"default\":0},\"total\":{\"type\":\"integer\",\"title\":\"total\",\"default\":0}},\"required\":[\"current\",\"hitCount\",\"pages\",\"records\",\"searchCount\",\"size\",\"total\"]},\"msg\":{\"type\":\"string\",\"title\":\"msg\",\"default\":\"操作成功\"},\"success\":{\"type\":\"boolean\",\"title\":\"success\",\"default\":true}},\"required\":[\"code\",\"data\",\"msg\",\"success\"]}
格式化后
{
"type": "object",
"title": "root",
"properties": {
"code": {
"type": "integer",
"title": "code",
"default": 0
},
"data": {
"type": "object",
"title": "data",
"properties": {
"current": {
"type": "integer",
"title": "current",
"default": 0
},
"hitCount": {
"type": "boolean",
"title": "hitCount",
"default": true
},
"pages": {
"type": "integer",
"title": "pages",
"default": 0
},
"records": {
"type": "array",
"title": "records",
"items": {
"type": "object",
"title": "items",
"properties": {
"api": {
"type": "string",
"title": "api",
"default": "接口"
},
"apiName": {
"type": "string",
"title": "apiName",
"default": "接口名称"
},
"appCode": {
"type": "string",
"title": "appCode",
"default": "应用code"
},
"appName": {
"type": "string",
"title": "appName",
"default": "应用名称"
},
"fieldTemplateId": {
"type": "string",
"title": "fieldTemplateId",
"default": "模板名称"
},
"jsonTemplate": {
"type": "string",
"title": "jsonTemplate",
"default": "接口结果模板"
},
"ruleConditionId": {
"type": "string",
"title": "ruleConditionId",
"default": "ShenYu的选择器规则ID"
},
"selectorId": {
"type": "string",
"title": "selectorId",
"default": "ShenYu的选择器Id"
}
},
"required": [
"api",
"apiName",
"appCode",
"appName",
"fieldTemplateId",
"jsonTemplate"
]
}
},
"searchCount": {
"type": "boolean",
"title": "searchCount",
"default": true
},
"size": {
"type": "integer",
"title": "size",
"default": 0
},
"total": {
"type": "integer",
"title": "total",
"default": 0
}
},
"required": [
"current",
"hitCount",
"pages",
"records",
"searchCount",
"size",
"total"
]
},
"msg": {
"type": "string",
"title": "msg",
"default": "操作成功"
},
"success": {
"type": "boolean",
"title": "success",
"default": true
}
},
"required": [
"code",
"data",
"msg",
"success"
]
}
我这边接口存的都是使用JSON.stringify()格式化后的数据,如果需要存对象的话可以修改组件即可
标签:vue,string,title,default,true,visual,json,type From: https://blog.csdn.net/qq_26014419/article/details/141932205