首页 > 其他分享 >使用vue-json-pretty和vue-json-schema-editor-visual封装一个可视化json数据编辑器及字段级别的权限管控

使用vue-json-pretty和vue-json-schema-editor-visual封装一个可视化json数据编辑器及字段级别的权限管控

时间:2024-09-05 15:54:03浏览次数:6  
标签:vue string title default true visual json type

一、前言

        最近做了一个需求,要求实现可以对json结构进行编辑保存及字段级别的权限管控,结合目前已有的轮子和当前项目的结构,决定使用vue-json-prettyvue-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

相关文章

  • java+vue计算机毕设社区独居老人健康管理系统【源码+开题+论文】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着我国人口老龄化的加速,独居老人的数量显著增加,这一群体在健康管理上面临着诸多挑战。传统的养老模式难以全面覆盖并有效满足独居老人的健康需求,特......
  • java+vue计算机毕设汽车租赁管理系统【源码+开题+论文】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着城市化进程的加速和居民生活水平的提高,汽车租赁作为一种便捷、灵活的出行方式,日益受到广大消费者的青睐。传统汽车租赁行业面临着管理效率低下、......
  • java+vue计算机毕设求职招聘管理系统【源码+开题+论文】
    本系统(程序+源码)带文档lw万字以上文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网的飞速发展,网络招聘已成为企业与求职者之间沟通的主要桥梁。传统的招聘方式,如招聘会、报纸广告等,不仅成本高、效率低,而且难以精准匹配企业......
  • 基于SpringBoot+Vue的高校宣讲会管理系统(带1w+文档)
    基于SpringBoot+Vue的高校宣讲会管理系统(带1w+文档)基于SpringBoot+Vue的高校宣讲会管理系统(带1w+文档)高校宣讲会管理系统可以对高校宣讲会管理系统信息进行集中管理,可以真正避免传统管理的缺陷。高校宣讲会管理系统是一款运用软件开发技术设计实现的应用系统,在......
  • 计算机毕业设计django+vue超市商品管理系统【开题+论文+程序】
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展和电子商务的普及,传统超市行业正面临着前所未有的变革与挑战。传统的商品管理模式依赖于大量的人工操作和纸质记录......
  • VUE环境搭建之安装nvm自由管理node的版本
    一、下载地址官网:https://github.com/coreybutler/nvm-windows/releases二、安装三、使用命令行查看nvmnvmlsavailable四、访问https://nodejs.org/en/about/previous-releases获取可以的node版本......
  • 这应该是全网最详细的Vue3.5版本解读kh
    合集-vue3代码修炼秘籍(16)1.答应我,在vue中不要滥用watch好吗?02-292.一文搞懂Vue3defineModel双向绑定:告别繁琐代码!02-043.没有虚拟DOM版本的vue(VueVapor)01-264.有了CompositionAPI后,有些场景或许你不需要pinia了01-235.你不知道的vue3:使用runWithContext实现在非setup期......
  • 基于nodejs+vue大学生社团活动平台[程序+论文+开题]-计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着高等教育的普及和大学生课余生活的日益丰富,社团活动已成为培养学生综合素质、促进人际交往、提升团队协作能力的重要载体。然而,传统的社团管理方式往往......
  • 基于nodejs+vue大学生食堂饭菜价格信息管理系统[程序+论文+开题]-计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着高校规模的不断扩大与学生群体的日益多元化,大学生食堂作为校园生活的重要组成部分,其饭菜价格管理成为了一个备受关注的议题。传统的手工记录与核算方式......
  • 基于nodejs+vue大学生数码租赁网站[程序+论文+开题]-计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着科技的飞速发展,数码产品已成为大学生学习、生活及娱乐不可或缺的一部分。然而,高昂的购置成本往往让经济尚未独立的学生群体望而却步。同时,数码产品的更......