vue.js 实现导入json解析成动态el-table树表格(接口文档功能)

49 篇文章 1 订阅
41 篇文章 0 订阅

一、需求描述:前段时间接到一个需求是做一个类似接口文档的显示功能,将一段json数据贴到里面就可以自动解析出json数据的每个字段的类型和层级关系,用element组件的树表格的形式展示,并且可以手动新增、修改和删除某个数据字段。

二、界面展示:功能如下图所示:

1.未贴数据之前:

2.点击右上角的‘导入json',在打开的弹框中贴入如下json数据:{"name":"lemon","sex":"女","age":18,"hobby":{"hobby1":"敲代码","hobby2":"跳恰恰"},"likeArr":["水果","青菜"]}

3.点击确认后树表格自动展示贴入的json数据,如下图所示;

4.点击每行的最右侧可以进行新增和删除操作;

5.点击tab切换到预览展示效果:

三、代码实现:

  1. 弹框代码展示,新建一个jsonDialog.vue文件,MonacoEditor是一个json编辑器,实现以下代码:

<template>
  <el-dialog
    title="导入 json"
    :visible.sync="dialogFormVisible"
    :close-on-click-modal="false"
    :modal-append-to-body="false"
    width="35%"
    @close="close"
    class="my_dialog"
  >
    <div class="empi_dialog_form">
      <!-- 返回 -->
      <div v-if="type == 'resp'">
        <monaco-editor v-model="jsonData" language="json" :readOnly="false"></monaco-editor>
      </div>
    </div>
    <span slot="footer" class="dialog-footer">
      <el-button @click="close">取 消</el-button>
      <el-button type="primary" @click="onSubmit()">确认</el-button>
    </span>
  </el-dialog>
</template>
<script>
export default {
  components: {
    MonacoEditor: () => import('@/components/MonacoEditor')
  },
  data() {
    return {
      dialogFormVisible: false,
      jsonData: null, //返回参数
    }
  },
  methods: {
    open() {
      this.dialogFormVisible = true
    },
    close() {
      this.dialogFormVisible = false
      this.jsonData = ''
    },
    // 提交
    onSubmit() {
      if (!this.jsonData) return this.$message.error('json数据不能为空')
      let flag = this.checkJson(data)
      if (flag) {
        this.dialogFormVisible = false
        this.$emit('getJson', data)
      } else {
        return this.$message.error('json数据格式不正确')
      }
    },
    // 判断是否是json格式
    checkJson(str) {
      if (typeof str == 'string') {
        try {
          let obj = JSON.parse(str)
          if (typeof obj == 'object' && obj) {
            return true
          } else {
            return false
          }
        } catch (e) {
          //console.log('error:' + str + '!!!' + e)
          return false
        }
      }
      //console.log('It is not a string!')
    }
  }
}
</script>
<style lang="scss" scoped>
.my_dialog {
  :deep(.el-dialog__body) {
    padding-bottom: 20px;
    height: 50vh;
    #code-editor {
      height: calc(50vh - 40px) !important;
    }
  }
}
</style>
  1. MonacoEditor是一个json编辑器,在components文件夹下新建MonacoEditor文件夹,MonacoEditor组件实现如下:

<template>
  <div>
    <div
      id="code-editor"
      style="
        width: 100%;
        height: 100%;
        min-height: 200px;
        border: 1px solid #dcdfe6;
      "
    ></div>
  </div>
</template>

<script>
// import * as monaco from 'monaco-editor'
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
import { checkJsonCode, getJsonPath } from '@/utils/monaco-editor.js'

export default {
  name: 'monacoEditor',
  model: {
    prop: 'content',
    event: 'change'
  },
  props: {
    content: null,
    language: {
      default: 'javascript'
    },
    readOnly: {
      default: false
    }
  },
  data() {
    return {
      editor: null,
      jsonPath: null
    }
  },
  watch: {
    content(newValue) {
      // console.debug("Code editor: content change");
      let value_ = newValue
      if (this.editor) {
        if (newValue !== this.editor.getValue()) {
          if (this.language == 'json') {
            value_ = checkJsonCode(value_)
          }
          monaco.editor.setModelLanguage(this.editor.getModel(), this.language)
          this.editor.trigger(
            this.editor.getValue(),
            'editor.action.formatDocument'
          )
          this.editor.setValue(value_)
        }
      }
    }
  },
  mounted() {
    const { copyToClipboard } = this
    let value_ = this.content
    if (this.language == 'json') {
      value_ = checkJsonCode(this.content)
    }
    this.editor = monaco.editor.create(this.$el.querySelector('#code-editor'), {
      value: value_,
      language: this.language,
      theme: 'vs',
      readOnly: this.readOnly,
      automaticLayout: true
    })
    this.editor.addAction({
      id: 'json-path',
      label: 'Copy JsonPath',
      keybindings: [
        monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_J)
      ],
      precondition: "editorLangId == 'json'",
      keybindingContext: "editorLangId == 'json'",
      contextMenuGroupId: '9_cutcopypaste',
      contextMenuOrder: 2,
      run: copyToClipboard
    })
    this.editor.onDidChangeModelContent((event) => {
      const value = this.editor.getValue()
      if (this.value !== value) {
        this.$emit('change', value, event)
      }
    })
    this.editor.onDidChangeCursorPosition((event) => {
      const value = this.editor.getValue()
      const offSet = this.editor.getModel().getOffsetAt(event.position)
      const { language } = this
      if (this.value !== value && language === 'json') {
        this.$emit('on-cursor-change', { offSet })
      }
      if (language == 'json' && offSet !== 0) {
        this.jsonPath = getJsonPath(value, offSet)
        this.$emit('on-jsonpath-change', { jsonPath: this.jsonPath })
      }
    })
  },
  methods: {
    copyToClipboard() {
      const notification = this.$Notice
      if (this.jsonPath) {
        navigator.clipboard.writeText(this.jsonPath).then(
          function () {},
          function () {
            notification.error({
              title: 'jsonpath copy failed.'
            })
          }
        )
      } else {
        notification.warning({
          title: 'There is no jsonpath that can be copied.'
        })
      }
    }
  }
}
</script>
  1. 新建utils/monaco-editor.js,实现以下代码:

const colType = { Object, Array }

export function getJsonPath(text, offSet) {
  let pos = 0
  const stack = []
  let isInKey = false

  while (pos < offSet) {
    const startPos = pos
    switch (text[pos]) {
      case '"':
        const { text: s, pos: newPos } = readString(text, pos)
        if (stack.length) {
          const frame = stack[stack.length - 1]
          if (frame.colType === colType.Object && isInKey) {
            frame.key = s
            isInKey = false
          }
        }
        pos = newPos
        break
      case '{':
        stack.push({ colType: colType.Object })
        isInKey = true
        break
      case '[':
        stack.push({ colType: colType.Array, index: 0 })
        break
      case '}':
      case ']':
        stack.pop()
        break
      case ',':
        if (stack.length) {
          const frame = stack[stack.length - 1]
          if (frame.colType === colType.Object) {
            isInKey = true
          } else {
            frame.index++
          }
        }
        break
    }
    if (pos === startPos) {
      pos++
    }
  }
  return pathToString(stack)
}

function pathToString(path) {
  let s = '$'
  try {
    for (const frame of path) {
      if (frame.colType === colType.Object) {
        if (!frame.key.match(/^[a-zA-Z$_][a-zA-Z\d$_]*$/)) {
          const key = frame.key.replace('"', '\\"')
          s += `["${frame.key}"]`
        } else {
          if (s.length) {
            s += '.'
          }
          s += frame.key
        }
      } else {
        s += `[${frame.index}]`
      }
    }
    return s
  } catch (ex) {
    return ''
  }
}

function isEven(n) {
  return n % 2 === 0
}

function readString(text, pos) {
  let i = pos + 1
  i = findEndQuote(text, i)
  const textpos = {
    text: text.substring(pos + 1, i),
    pos: i + 1
  }
  return textpos
}

// Find the next end quote
function findEndQuote(text, i) {
  while (i < text.length) {
    // console.log('findEndQuote: ' + i + ' : ' + text[i])
    if (text[i] === '"') {
      let bt = i
      // Handle backtracking to find if this quote is escaped (or, if the escape is escaping a slash)
      while (bt >= 0 && text[bt] == '\\') {
        bt--
      }
      if (isEven(i - bt)) {
        break
      }
    }
    i++
  }
  return i
}

export function checkJsonCode(strJsonCode) {
  let res = ''
  try {
    for (let i = 0, j = 0, k = 0, ii, ele; i < strJsonCode.length; i++) {
      ele = strJsonCode.charAt(i)
      if (j % 2 === 0 && ele === '}') {
        // eslint-disable-next-line no-plusplus
        k--
        for (ii = 0; ii < k; ii++) ele = `    ${ele}`
        ele = `\n${ele}`
      } else if (j % 2 === 0 && ele === '{') {
        ele += '\n'
        // eslint-disable-next-line no-plusplus
        k++
        for (ii = 0; ii < k; ii++) ele += '    '
      } else if (j % 2 === 0 && ele === ',') {
        ele += '\n'
        for (ii = 0; ii < k; ii++) ele += '    '
        // eslint-disable-next-line no-plusplus
      } else if (ele === '"') j++
      res += ele
    }
  } catch (error) {
    res = strJsonCode
  }
  return res
}
  1. 界面代码展示,新建一个jsonIndex.vue界面,实现以下代码:

<!-- 返回数据设置 -->
<div class="panel-item">
    <div class="panel-item-title">返回参数</div>
    <el-radio-group v-model="checkRespLabel"
        size="mini" class="radio_btn_group">
        <el-radio-button label="JSON">
        </el-radio-button>
    </el-radio-group>
    <div class="panel-item-tab">
        <div class="blue json-btn" v-show="activeTabName == 'first'" @click="addJsonClick('resp')" > 添加 </div>
        <div class="blue json-btn" v-show="activeTabName == 'first'" @click="toJsonClick('resp')"> 导入json </div>
        <el-tabs v-model="activeTabName" type="card" class="card-tab">
            <el-tab-pane label="模板" name="first">
                <el-table
                    :data="threeStepData.responseParams"
                    class="json-table"
                    :show-header="false"
                    :highlight-current-row="false"
                    row-key="id" size="medium"
                    default-expand-all
                    :tree-props="{children: 'children',hasChildren: 'hasChildren'}">
                    <el-table-column label="参数名称">
                        <template slot-scope="scopes">
                            <el-input placeholder="name" v-model="scopes.row.jsonName">
                            </el-input>
                        </template>
                    </el-table-column>
                    <el-table-column label="参数类型">
                        <template slot-scope="scopes">
                            <el-select v-model="scopes.row.jsonType" placeholder="type">
                                <el-option
                                    v-for="item in typeData"
                                    :key="item.value"
                                    :label="item.label"
                                    :value="item.value">
                                </el-option>
                            </el-select>
                        </template>
                    </el-table-column>
                    <el-table-column label="备注">
                        <template slot-scope="scopes">
                            <el-input placeholder="备注" v-model="scopes.row.jsonRemark">
                            </el-input>
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" width="150">
                        <template slot-scope="scopes">
                            <el-tooltip
                                class="item"
                                effect="dark"
                                content="删除节点"
                                placement="top" :open-delay="500">
                                <i class="blue el-icon-close" @click="removeJsonClick(scopes.row, 'resp')"></i>
                            </el-tooltip>
                            <el-tooltip
                                class="item"
                                effect="dark"
                                content="添加子节点"
                                placement="top" :open-delay="500">
                                <i class="blue el-icon-plus" @click="addJsonChildrenClick(scopes.row, 'resp')"></i>
                            </el-tooltip>
                        </template>
                    </el-table-column>
                </el-table>
            </el-tab-pane>
            <el-tab-pane label="预览" name="second">
                <div class="panel-item-content">
                    <el-input type="textarea" disabled :rows="5" v-model="strParams">
                    </el-input>
                </div>
            </el-tab-pane>
        </el-tabs>
    </div>
  </div>
//弹框
<jsonDialog ref="jsonDialog" @getJson="getJson"></jsonDialog>

  1. 展示界面jsonIndex.vue的功能代码,对导入json的展示及相关操作的实现:

<script>
export default {
    components: {
        MonacoEditor: () => import('@/components/MonacoEditor'),
        jsonDialog: () => import('./../dialog/jsonDialog')
    },
     data() {
        return {
            threeStepData: {
                responseParams: [
                    // {
                    //     id: 1,
                    //     jsonName: 'root',
                    //     jsonType: 'object',
                    //     jsonRemark: '备注',
                    //     pid: 0,
                    //     children: []
                    // }
                ]
            },
            checkRespLabel: 'JSON',
            activeTabName: 'first',
            typeData: [
                { label: 'string', value: 'string' },
                { label: 'number', value: 'number' },
                { label: 'array', value: 'array' },
                { label: 'object', value: 'object' },
                { label: 'boolean', value: 'boolean' }
            ]
        }
    },
    computed: {
        strParams() {
            return this.threeStepData?.responseParams
                ? JSON.stringify(this.threeStepData.responseParams)
                : '-'
        },
    },
    methods: {
        open(data) {
            this.threeStepData = data
        },
        // 导入json
        toJsonClick(type) {
            this.$refs.jsonDialog.open(type)
        },
        // 生成唯一id
        guid() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
                /[xy]/g,
                function (c) {
                    let r = (Math.random() * 16) | 0,
                        v = c == 'x' ? r : (r & 0x3) | 0x8
                    return v.toString(16)
                }
            )
        },
        // 获取json导入数据
        getJson(data, type) {
            let _data = JSON.parse(data)
            let _type = this.getJsonType(_data)
            let arr = []
            if (_type === 'object') {
                arr = this.handleJson(_data)
            }
            if (type == 'resq') {
                this.threeStepData.responseParams = arr
                // this.threeStepData.responseParams[0].children = arr
            }
        },
        // json导入数据转换
        handleJson(data) {
            let arr = []
            Object.keys(data).map((key) => {
                let _type = this.getJsonType(data[key])
                if (_type && _type == 'object') {
                    let children = this.handleJson(data[key])
                    arr.push({
                        id: this.guid(),
                        pid: data.id,
                        jsonName: key,
                        jsonType: _type,
                        jsonRemark: '',
                        children
                    })
                } else {
                    arr.push({
                        id: this.guid(),
                        jsonName: key,
                        jsonType: _type,
                        jsonRemark: ''
                    })
                }
            })
            return arr
        },
        // 判断数据类型
        getJsonType(data) {
            let type = Object.prototype.toString.call(data)
            if (type === '[object String]') {
                type = 'string'
            } else if (type === '[object Number]') {
                type = 'number'
            } else if (type === '[object Null]') {
                type = 'null'
            } else if (type === '[object Boolean]') {
                type = 'boolean'
            } else if (type === '[object Array]') {
                type = 'array'
            } else if (type === '[object Object]') {
                type = 'object'
            } else {
                type = '未进行判断的类型:' + type
            }
            return type
        },

        // 新增json数据
        addJsonClick(type) {
            if(type=='resp'){
                // if(this.threeStepData.responseParams?.length==1){
                //     this.$message.closeAll();
                //     this.$message.error('请勿重复添加根节点!');
                //     return;
                // }
                let obj = {
                    id: this.guid(),
                    jsonName: '',
                    jsonType: 'object',
                    jsonRemark: '',
                    // pid: 0,
                    children: []
                }
                this.threeStepData.responseParams.push(obj)
            }
        },
        //添加子节点
        addJsonChildrenClick(data, type) {
            let obj = {
                id: this.guid(),
                jsonName: '',
                jsonType: 'string',
                jsonRemark: '',
                pid: data.id
            }
           let node = this.addNode(this.threeStepData.responseParams, data.id, obj)
           if (type === 'resp') {
                this.threeStepData.responseParams = JSON.parse(JSON.stringify(node))
            }
        },
        addNode(list, pid, obj) {
            list.forEach((e) => {
                if (e.id == pid) {
                    e.children ? e.children.push(obj) : (e.children = [obj])
                } else {
                    if (e.children && e.children.length > 0) {
                        this.addNode(e.children, pid, obj)
                    }
                }
            })
            return list
        },
        // 移除json数据
        removeJsonClick(data, type) {
            let objMap = {
                resp: this.threeStepData.responseParams,
            }
            let node = this.removeItem(objMap[type], data.id)
            if (type === 'resp') {
                this.threeStepData.responseParams = JSON.parse(JSON.stringify(node))
            }
        },
        removeItem(root, id) {
            root.forEach((e, i) => {
                if (e.id === id) {
                    root.splice(i, 1)
                } else if (e.children && e.children.length > 0) {
                    this.removeItem(e.children, id)
                }
            })
            return root
        }
    }
}
</script>
  1. 展示界面jsonIndex.vue的相关样式代码如下:

<style lang="scss" scoped>
:deep(.el-col) {
  img {
    width: 20px;
    height: 16px;
    cursor: ns-resize;
  }
  .el-input,
  .el-select {
    width: 100%;
    .el-input__inner {
      height: 40px;
      line-height: 40px;
    }
  }
  i {
    font-size: 14px;
  }
}

.panel-item {
  .panel-item-cell {
    padding: 5px 10px;
    border: 1px solid #e6eaef;
  }
  .panel-item-container{
    border: 1px solid #e6eaef;
    border-top:none;
    min-height: 80px;
    padding:10px 10px 20px 10px;
    .empty-contents{
        height: 40px;
        line-height: 40px;
        color: #909399;
        font-size: 14px; 
        width: 100%;
        text-align: center;
        margin-top: 10px;
    }
    .panel-item-row {
        padding-top: 20px;
        .el-col {
        i {
            font-size: 20px;
        }
        }
    }
  }
  .panel-item-tab {
    position: relative;
    .json-btn {
      position: absolute;
      top: 16px;
      right: 10px;
      cursor: pointer;
      z-index: 9;
    }
  }
}
.card-tab {
  :deep(.el-tabs__item) {
    padding: 0 36px !important;
  }
}
.json-table {
  padding: 20px;
  &::before {
    height: 0;
  }
  :deep(.el-table__body-wrapper) {
    .el-table__cell {
      border: 0;
      .cell {
        display: flex;
        align-items: center;
        .el-table__expand-icon {
          //   width: 60px;
          font-size: 14px;
        }
        .el-table__indent {
          //   padding-left: 70px !important;
        }
        .el-input,
        .el-select {
          height: 40px;
          width: 100%;
          .el-input__inner {
            height: 100% !important;
          }
        }
        > i {
          width: 33%;
          cursor: pointer;
        }
      }
    }
  }
}
</style>

综上所述,已经完成了json数据的展示、修改和新增删除都已经完成,可能有些错误,欢迎大家指正~

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值