一、需求描述:前段时间接到一个需求是做一个类似接口文档的显示功能,将一段json数据贴到里面就可以自动解析出json数据的每个字段的类型和层级关系,用element组件的树表格的形式展示,并且可以手动新增、修改和删除某个数据字段。
二、界面展示:功能如下图所示:
1.未贴数据之前:
![](https://i-blog.csdnimg.cn/blog_migrate/7c2ff0f00bc95d910d67fed945f0e7c2.png)
2.点击右上角的‘导入json',在打开的弹框中贴入如下json数据:{"name":"lemon","sex":"女","age":18,"hobby":{"hobby1":"敲代码","hobby2":"跳恰恰"},"likeArr":["水果","青菜"]}
![](https://i-blog.csdnimg.cn/blog_migrate/18a68743c2376006999a745f47aa3084.png)
3.点击确认后树表格自动展示贴入的json数据,如下图所示;
![](https://i-blog.csdnimg.cn/blog_migrate/099e78bad2305d67c035a33b7fb82c9d.png)
4.点击每行的最右侧可以进行新增和删除操作;
![](https://i-blog.csdnimg.cn/blog_migrate/65ef62befe7e3c11d8ad7450ad40bd33.png)
![](https://i-blog.csdnimg.cn/blog_migrate/62f8190d09ec55c67ae79b58d45aa219.png)
5.点击tab切换到预览展示效果:
![](https://i-blog.csdnimg.cn/blog_migrate/74b7089ef9758a0f3d5ec2e3b51cabc7.png)
三、代码实现:
弹框代码展示,新建一个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>
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>
新建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
}
界面代码展示,新建一个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>
展示界面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>
展示界面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数据的展示、修改和新增删除都已经完成,可能有些错误,欢迎大家指正~