当前位置:   article > 正文

Vue实现JSON字符串格式化编辑器组件_vue json组件

vue json组件

相信很多同学都用过网上的在线JSON格式化工具来将杂乱的JSON数据转换成易于我们阅读和编辑的格式。那么,你有没有想过自己动手实现一个这样的工具呢?今天,我将介绍如何使用Vue.js来构建一个简单的JSON格式化工具。

功能简述

  • 支持格式化JSON字符串
  • 支持去除字符串中的空格
  • 支持全屏操作
  • 实时展示格式化状态
  • 控制台展示成功和失败的详情,支持错误定位
  • 编辑器精准计算字符串的行号

效果图展示

默认

全屏

功能介绍

按钮

其他

1、自动补全

输入”(“、”{“、”[“将会自动补全另一半

2、自动删除

删除括号时也会自动删除另一半

3、括号匹配

点击括号会高亮另一半括号,方便定位

4、支持ctrl+z撤销和ctrl+y重做功能

5、编辑器根据字符串的换行计算行号并展示

代码

vue文件
  1. <!--JsonEditor.vue-->
  2. <template>
  3. <div ref="center" id="editor_body" :style="{ height: editorHeight, width: editorWidth }">
  4. <div style="height: 80%">
  5. <div class="tool_slider">
  6. <div style="display: flex; align-items: center">
  7. <img
  8. src="@/assets/icons/format.svg"
  9. class="icon_hover"
  10. @click="prettyFormat(viewJsonStr)"
  11. title="格式化"
  12. />
  13. <div style="height: 18px; border: 1px solid #858585; margin: 0 3px"></div>
  14. <img
  15. src="@/assets/icons/clearLine.svg"
  16. class="icon_hover"
  17. @click="viewJsonStr = viewJsonStr.replace(/\s+/g, '')"
  18. title="去除空格"
  19. />
  20. <div
  21. style="
  22. display: flex;
  23. align-items: center;
  24. border-left: 2px solid #858585;
  25. height: 18px;
  26. margin: 0 3px;
  27. padding: 0 3px;
  28. "
  29. >
  30. <img
  31. src="@/assets/icons/full.svg"
  32. v-if="!isFullScreen"
  33. class="icon_hover"
  34. @click="fullScreen"
  35. title="全屏"
  36. />
  37. <img
  38. src="@/assets/icons/closeFull.svg"
  39. v-else
  40. class="icon_hover"
  41. @click="fullScreen"
  42. title="退出"
  43. />
  44. </div>
  45. </div>
  46. <div style="display: flex; align-items: center">
  47. <img
  48. src="@/assets/icons/success.svg"
  49. title="格式正确"
  50. v-if="isPass"
  51. style="height: 20px; width: 20px"
  52. />
  53. <img
  54. src="@/assets/icons/error.svg"
  55. title="格式错误"
  56. v-else
  57. style="height: 17px; width: 17px"
  58. />
  59. </div>
  60. </div>
  61. <div class="edit-container">
  62. <textarea
  63. wrap="off"
  64. cols="1"
  65. id="leftNum"
  66. disabled
  67. onscroll="document.getElementById('rightNum').scrollTop = this.scrollTop;"
  68. ></textarea>
  69. <textarea
  70. ref="myTextarea"
  71. id="rightNum"
  72. :key="isFullScreen"
  73. style="width: 100%"
  74. placeholder="请输入JSON字符串"
  75. onscroll="document.getElementById('leftNum').scrollTop = this.scrollTop;"
  76. :value="viewJsonStr"
  77. @click="handleClick"
  78. @input="handleTextareaInput1"
  79. />
  80. </div>
  81. </div>
  82. <div id="console">{{ jsonObj }}</div>
  83. </div>
  84. </template>
  85. <script lang="ts" setup>
  86. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
  87. import { cloneDeep } from 'lodash-es';
  88. import {
  89. handleBackspace,
  90. handleClick,
  91. handleClickEnter,
  92. handleTabKey,
  93. handleTextareaInput,
  94. setAutoKey,
  95. } from '@/components/JsonEditor';
  96. const emit = defineEmits(['update:value']);
  97. const props = defineProps({
  98. value: {
  99. type: String,
  100. default: '',
  101. },
  102. width: {
  103. type: String,
  104. default: '1000px',
  105. },
  106. height: {
  107. type: String,
  108. default: '400px',
  109. },
  110. });
  111. const viewJsonStr: any = ref(props.value);
  112. const editorWidth: any = ref(JSON.parse(JSON.stringify(props.width)));
  113. const editorHeight: any = ref(JSON.parse(JSON.stringify(props.height)));
  114. // 自动补全
  115. function handleTextareaInput1(event) {
  116. handleTextareaInput(viewJsonStr, event);
  117. }
  118. const isPass = ref(true);
  119. watch(
  120. () => viewJsonStr.value,
  121. (newValue) => {
  122. calculateNum(newValue);
  123. emit('update:value', newValue);
  124. },
  125. );
  126. const num = ref('');
  127. function calculateNum(value) {
  128. let lineBbj: any = document.getElementById('leftNum');
  129. num.value = '';
  130. let str = value;
  131. if (str === null || str === undefined) {
  132. str = '';
  133. }
  134. str = str.replace(/\r/gi, '');
  135. str = str.split('\n');
  136. let n = str.length;
  137. if (n.toString().length > 3) {
  138. lineBbj.style.width = n.toString().length * 10 + 'px';
  139. } else {
  140. lineBbj.style.width = '30px';
  141. }
  142. for (let i = 1; i <= n; i++) {
  143. if (document.all) {
  144. num.value += i + '\r\n'; //判断浏览器是否是IE
  145. } else {
  146. num.value += i + '\n';
  147. }
  148. }
  149. lineBbj.value = num.value;
  150. }
  151. // 预览对象
  152. const jsonObj = computed(() => {
  153. const str = cloneDeep(viewJsonStr.value);
  154. // 如果输入的全是数字,JSON.parse(str)不会报错,需要手动处理一下
  155. const onlyNumber = /^\d+$/.test(str);
  156. const dom = document.getElementById('console');
  157. function setColor(color) {
  158. if (dom) {
  159. dom.style.color = color;
  160. }
  161. }
  162. if (str) {
  163. try {
  164. if (onlyNumber) {
  165. setColor('red');
  166. isPass.value = false;
  167. return getCurrentTime() + str + ' is not valid JSON';
  168. }
  169. setColor('black');
  170. isPass.value = true;
  171. if (JSON.parse(str)) {
  172. setColor('green');
  173. return `${getCurrentTime()}校验通过`;
  174. }
  175. } catch (e: any) {
  176. isPass.value = false;
  177. setColor('red');
  178. if (e.message?.match(/position\s+(\d+)/)) {
  179. const location = e.message?.match(/position\s+(\d+)/)[1];
  180. const str1 = str.substring(0, location).trim();
  181. const str2 = str1.split('\n');
  182. const message = e.message.substring(0, e.message.indexOf('position'));
  183. // 如果当前行或者前一行有'['
  184. if (str2[str2.length - 1]?.includes('[')) {
  185. const { line, column } = getLineAndColumn(str1, str1.length - 1);
  186. return `${message} at line ${line},column ${column}`;
  187. }
  188. const { line, column } = getLineAndColumn(str, location);
  189. return `${getCurrentTime()}${message} at line ${line},column ${column}`;
  190. } else {
  191. return getCurrentTime() + str + ' is not valid JSON';
  192. }
  193. }
  194. } else {
  195. return null;
  196. }
  197. });
  198. // 获取当前时间
  199. function getCurrentTime() {
  200. let now = new Date(); // 获取当前日期和时间
  201. let hours = now.getHours(); // 获取小时
  202. let minutes: string | number = now.getMinutes(); // 获取分钟
  203. let seconds: string | number = now.getSeconds(); // 获取秒
  204. let period = hours >= 12 ? '下午' : '上午'; // 判断是上午还是下午
  205. // 将小时转换为12小时制
  206. hours = hours % 12 || 12;
  207. // 格式化分钟和秒,确保它们是两位数
  208. minutes = minutes < 10 ? '0' + minutes : minutes;
  209. seconds = seconds < 10 ? '0' + seconds : seconds;
  210. // 构造最终的时间字符串
  211. let currentTime = period + hours + ':' + minutes + ':' + seconds;
  212. return '【' + currentTime + '】 ';
  213. }
  214. //计算错误信息所在行列
  215. function getLineAndColumn(str, index) {
  216. let line = 1;
  217. let column = 1;
  218. for (let i = 0; i < index; i++) {
  219. if (str[i] === '\n') {
  220. line++;
  221. column = 1;
  222. } else {
  223. column++;
  224. }
  225. }
  226. return { line, column };
  227. }
  228. //json格式美化
  229. function prettyFormat(str) {
  230. try {
  231. // 设置缩进为2个空格
  232. str = JSON.stringify(JSON.parse(str), null, 4);
  233. str = str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
  234. viewJsonStr.value = str.replace(
  235. /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
  236. function (match) {
  237. return match;
  238. },
  239. );
  240. } catch (e) {
  241. console.log('异常信息:' + e);
  242. }
  243. }
  244. const center = ref();
  245. const isFullScreen = ref(false);
  246. // 添加或删除全屏属性
  247. function fullScreen() {
  248. if (center.value) {
  249. if (center.value.className.includes('fullScreen')) {
  250. editorHeight.value = JSON.parse(JSON.stringify(props.height));
  251. editorWidth.value = JSON.parse(JSON.stringify(props.width));
  252. center.value.className = center.value.className.replace(' fullScreen', '');
  253. isFullScreen.value = false;
  254. } else {
  255. editorHeight.value = '100vh';
  256. editorWidth.value = '100vw';
  257. center.value.className += ' fullScreen';
  258. isFullScreen.value = true;
  259. }
  260. }
  261. }
  262. const myTextarea: any = ref(null);
  263. function handleKeyDown(event) {
  264. if (myTextarea.value) {
  265. if (event.key === 'Backspace') {
  266. handleBackspace(viewJsonStr, event);
  267. } else if (event.key === 'Enter') {
  268. handleClickEnter(viewJsonStr, event);
  269. } else if (event.key === 'Tab') {
  270. handleTabKey(event);
  271. } else if (event.key === 'Escape') {
  272. if (isFullScreen.value) {
  273. fullScreen();
  274. }
  275. }
  276. }
  277. }
  278. // 符号自动补全以及选中文本后输入符号自动包裹
  279. function getMouseCheck(event) {
  280. setAutoKey(viewJsonStr, event);
  281. }
  282. onMounted(() => {
  283. window.addEventListener('keydown', handleKeyDown);
  284. document.addEventListener('keydown', getMouseCheck);
  285. calculateNum(props.value);
  286. });
  287. onBeforeUnmount(() => {
  288. window.removeEventListener('keydown', handleKeyDown);
  289. document.removeEventListener('keydown', getMouseCheck);
  290. });
  291. </script>
  292. <style scoped lang="less">
  293. #editor_body {
  294. border: 1px solid #d9d9d9;
  295. border-radius: 4px;
  296. padding: 5px;
  297. box-sizing: border-box;
  298. }
  299. .tool_slider {
  300. padding-left: 5px;
  301. padding-right: 5px;
  302. display: flex;
  303. width: 100%;
  304. box-sizing: border-box;
  305. justify-content: space-between;
  306. align-items: center;
  307. height: 25px;
  308. border: 1px solid #d9d9d9;
  309. border-bottom: 0;
  310. }
  311. .icon_hover {
  312. height: 20px;
  313. width: 20px;
  314. cursor: pointer;
  315. &:hover {
  316. color: #5c82ff;
  317. }
  318. }
  319. #leftNum {
  320. overflow: hidden;
  321. padding: 3px 2px;
  322. height: 100%;
  323. width: 30px;
  324. line-height: 22px;
  325. font-size: 13px;
  326. color: rgba(0, 0, 0, 0.25);
  327. font-weight: bold;
  328. resize: none;
  329. text-align: center;
  330. outline: none;
  331. border: 0;
  332. background: #f5f7fa;
  333. box-sizing: border-box;
  334. border-right: 1px solid;
  335. font-family: 微软雅黑;
  336. }
  337. #rightNum {
  338. white-space: nowrap;
  339. height: 100%;
  340. padding: 3px;
  341. line-height: 22px;
  342. box-sizing: border-box;
  343. resize: none;
  344. border: 0;
  345. font-family: 微软雅黑;
  346. &::-webkit-scrollbar {
  347. width: 5px;
  348. height: 5px;
  349. background-color: #efeae6;
  350. }
  351. &:focus-visible {
  352. outline: none;
  353. }
  354. &:hover {
  355. border: 0;
  356. }
  357. &:focus {
  358. border: 0;
  359. }
  360. }
  361. .leftBox {
  362. height: 100%;
  363. text-align: left;
  364. }
  365. .edit-container {
  366. height: calc(100% - 25px);
  367. width: 100%;
  368. box-sizing: border-box;
  369. border: 1px solid #d9d9d9;
  370. display: flex;
  371. }
  372. .fullScreen {
  373. position: fixed;
  374. z-index: 9999;
  375. left: 0;
  376. top: 0;
  377. right: 0;
  378. bottom: 0;
  379. background-color: #f5f7fa;
  380. }
  381. #console {
  382. padding: 12px;
  383. box-sizing: border-box;
  384. height: calc(20% - 5px);
  385. margin-top: 5px;
  386. width: 100%;
  387. background-color: white;
  388. border: 1px solid #d9d9d9;
  389. overflow: auto;
  390. font-family: 微软雅黑;
  391. text-align: left;
  392. }
  393. </style>
配置文件
  1. /*index.ts*/
  2. import { nextTick } from 'vue';
  3. // 获取文本框的值
  4. export const handleTextareaInput = (viewJsonStr, event) => {
  5. const textarea = event.target;
  6. const newValue = textarea.value;
  7. viewJsonStr.value = newValue;
  8. };
  9. // 设置自动补全
  10. export const setAutoKey = (viewJsonStr, event) => {
  11. const textarea: any = document.getElementById('rightNum');
  12. if (event.key === "'" || event.key === '"') {
  13. event.preventDefault(); // 阻止默认行为
  14. const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
  15. const newText = `${event.key}` + selectedText + `${event.key}`;
  16. const cursorPosition = textarea.selectionStart + 1;
  17. textarea.value =
  18. textarea.value.substring(0, textarea.selectionStart) +
  19. newText +
  20. textarea.value.substring(textarea.selectionEnd);
  21. textarea.setSelectionRange(cursorPosition, cursorPosition);
  22. } else if (event.key === '(') {
  23. event.preventDefault(); // 阻止默认行为
  24. const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
  25. const newText = '(' + selectedText + ')';
  26. const cursorPosition = textarea.selectionStart + 1;
  27. textarea.value =
  28. textarea.value.substring(0, textarea.selectionStart) +
  29. newText +
  30. textarea.value.substring(textarea.selectionEnd);
  31. textarea.setSelectionRange(cursorPosition, cursorPosition);
  32. } else if (event.key === '[') {
  33. event.preventDefault(); // 阻止默认行为
  34. const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
  35. const newText = '[' + selectedText + ']';
  36. const cursorPosition = textarea.selectionStart + 1;
  37. textarea.value =
  38. textarea.value.substring(0, textarea.selectionStart) +
  39. newText +
  40. textarea.value.substring(textarea.selectionEnd);
  41. textarea.setSelectionRange(cursorPosition, cursorPosition);
  42. } else if (event.key === '{') {
  43. event.preventDefault(); // 阻止默认行为
  44. const selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
  45. const newText = '{' + selectedText + '}';
  46. const cursorPosition = textarea.selectionStart + 1;
  47. textarea.value =
  48. textarea.value.substring(0, textarea.selectionStart) +
  49. newText +
  50. textarea.value.substring(textarea.selectionEnd);
  51. textarea.setSelectionRange(cursorPosition, cursorPosition);
  52. }
  53. viewJsonStr.value = textarea.value;
  54. };
  55. /*------------------------------------------------括号高亮------------------------------------------------------------*/
  56. const findOpeningBracketIndex = (text, startIndex, char) => {
  57. const openingBrackets = {
  58. ']': '[',
  59. '}': '{',
  60. ')': '(',
  61. };
  62. let count = 0;
  63. for (let i = startIndex; i >= 0; i--) {
  64. if (text.charAt(i) === char) {
  65. count++;
  66. } else if (text.charAt(i) === openingBrackets[char]) {
  67. count--;
  68. if (count === 0) {
  69. return i;
  70. }
  71. }
  72. }
  73. return -1;
  74. };
  75. const findClosingBracketIndex = (text, startIndex, char) => {
  76. const closingBrackets = {
  77. '[': ']',
  78. '{': '}',
  79. '(': ')',
  80. };
  81. let count = 0;
  82. for (let i = startIndex; i < text.length; i++) {
  83. if (text.charAt(i) === char) {
  84. count++;
  85. } else if (text.charAt(i) === closingBrackets[char]) {
  86. count--;
  87. if (count === 0) {
  88. return i;
  89. }
  90. }
  91. }
  92. return -1;
  93. };
  94. const isBracket = (char) => {
  95. return ['[', ']', '{', '}', '(', ')'].includes(char);
  96. };
  97. // 点击括号寻找对应另一半
  98. export const handleClick = (event) => {
  99. const textarea: any = document.getElementById('rightNum');
  100. const { selectionStart, selectionEnd, value } = textarea;
  101. const clickedChar = value.charAt(selectionStart);
  102. if (isBracket(clickedChar)) {
  103. const openingBracketIndex = findOpeningBracketIndex(value, selectionStart, clickedChar);
  104. const closingBracketIndex = findClosingBracketIndex(value, selectionStart, clickedChar);
  105. if (openingBracketIndex !== -1) {
  106. textarea.setSelectionRange(openingBracketIndex, openingBracketIndex + 1);
  107. } else if (closingBracketIndex !== -1) {
  108. textarea.setSelectionRange(closingBracketIndex, closingBracketIndex + 1);
  109. }
  110. }
  111. };
  112. /*键盘事件*/
  113. export function handleClickEnter(viewJsonStr, event) {
  114. if (event.key == 'Enter') {
  115. const textarea = event.target;
  116. const cursorPosition: any = textarea.selectionStart; // 获取光标位置
  117. const value = textarea.value;
  118. if (
  119. (value[cursorPosition - 1] === '{' && value[cursorPosition] == '}') ||
  120. (value[cursorPosition - 1] === '[' && value[cursorPosition] == ']')
  121. ) {
  122. textarea.value = value.slice(0, cursorPosition) + '\n' + value.slice(cursorPosition);
  123. textarea.setSelectionRange(cursorPosition, cursorPosition);
  124. viewJsonStr.value = textarea.value;
  125. // 将光标移动到插入的空格后面
  126. setTimeout(() => {
  127. handleTabKey(syntheticEvent);
  128. }, 30);
  129. }
  130. }
  131. }
  132. // 新建tab按键对象
  133. const syntheticEvent = new KeyboardEvent('keydown', {
  134. key: 'Tab',
  135. });
  136. // 按下tab键时的操作
  137. export const handleTabKey = (event) => {
  138. const textarea: any = document.getElementById('rightNum');
  139. const { selectionStart, selectionEnd } = textarea;
  140. const tabSpaces = ' '; // 4 spaces
  141. event.preventDefault();
  142. // 在当前光标位置插入4个空格
  143. textarea.value =
  144. textarea.value.substring(0, selectionStart) +
  145. tabSpaces +
  146. textarea.value.substring(selectionEnd);
  147. // 将光标向右移动4个空格
  148. textarea.selectionStart = selectionStart + tabSpaces.length;
  149. textarea.selectionEnd = selectionStart + tabSpaces.length;
  150. };
  151. // 按下Backspace按键时
  152. export function handleBackspace(viewJsonStr, event) {
  153. const textarea = event.target;
  154. const cursorPosition = textarea.selectionStart;
  155. const textBeforeCursor = viewJsonStr.value.slice(0, cursorPosition);
  156. const textAfterCursor = viewJsonStr.value.slice(cursorPosition);
  157. if (
  158. (textBeforeCursor.endsWith('"') && textAfterCursor.startsWith('"')) ||
  159. (textBeforeCursor.endsWith("'") && textAfterCursor.startsWith("'")) ||
  160. (textBeforeCursor.endsWith('[') && textAfterCursor.startsWith(']')) ||
  161. (textBeforeCursor.endsWith('{') && textAfterCursor.startsWith('}')) ||
  162. (textBeforeCursor.endsWith('(') && textAfterCursor.startsWith(')'))
  163. ) {
  164. event.preventDefault(); // 阻止默认的删除行为
  165. viewJsonStr.value = textBeforeCursor.slice(0, -1) + textAfterCursor.slice(1);
  166. nextTick(() => {
  167. textarea.selectionStart = cursorPosition - 1;
  168. textarea.selectionEnd = cursorPosition - 1;
  169. }).then((r) => {});
  170. }
  171. }
调用方式
  1. <JsonEditor v-model:value="testStr" />
  2. const testStr = ref('123');

总结

这个JSON编辑器不仅能够让你方便地格式化JSON字符串,还能帮你去掉不必要的空格。而且,它的全屏功能让编辑更加顺畅。最酷的是,它还能实时告诉你格式化的进度,如果遇到问题了,控制台会详细告诉你哪里出错了,这样你就能快速找到问题并解决它。编辑器还能精确地计算行号,这对于查找问题也是很有帮助的。而且,它还有自动补全、自动删除和括号匹配这些贴心的功能,让你的编辑工作变得更加轻松。如果你不小心做错了,也不用担心,因为它支持撤销和重做。希望它能帮助到大家,让我们的工作更加愉快!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/329518
推荐阅读
相关标签
  

闽ICP备14008679号