JSON Viewer

格式化显示JSON使数据看起来更加漂亮,支持折叠/展开格式化后的数据,支持JSON脑图让调用层级看着更清晰,支持复制JSON脑图节点路径

当前为 2024-10-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @license MIT
  3. // @name JSON Viewer
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.5.4
  6. // @note v0.5.4 单击复制修改为CTRL+单击复制JSONPath功能,JSON格式化风格增加table格式
  7. // @note v0.5.3 增加暗黑主题色
  8. // @note v0.5.2 单击JSON格式化的key可复制JSONPath
  9. // @note v0.5.1 修复JSONPath提示有误
  10. // @note v0.5.0 解决chrome 120+以上内核JSON格式化不执行和引入layer报错问题
  11. // @note v0.4.9 布局修改,增加保存JSON/脑图为文件,增加JSON过滤,鼠标移入key提示JSONPath
  12. // @note v0.4.8 代码优化
  13. // @note v0.4.7 增加对JSONP的判断,代码优化
  14. // @note v0.4.6 增加复制按钮,JSON脑图CSS样式细节优化,JSON脑图增加收起/展开子节点按钮
  15. // @note v0.4.5 在json-viewer-updated原基础上进行了一些修改,主要有CSS样式修改,新增折叠/展开全部功能,新增JSON脑图功能,脑图节点点击显示调用路径
  16. // @description 格式化显示JSON使数据看起来更加漂亮,支持折叠/展开格式化后的数据,支持JSON脑图让调用层级看着更清晰,支持复制JSON脑图节点路径
  17. // @author Feny
  18. // @match *://*/*
  19. // @grant GM_addStyle
  20. // @grant GM_getValue
  21. // @grant GM_setValue
  22. // @grant unsafeWindow
  23. // @grant GM_setClipboard
  24. // @grant GM_getResourceText
  25. // @icon data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAeAB4AAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAAgACADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9wvjF8bLX4ZrHZx+XNqlwnmKjH5YUyRvb6kEAd8H058d1b47XV5Ir3eoN++bagaXYrN6KOBn2FfPPx5/aEutX8a+KNWhIubhruZLSNj8u1WMcIOP4QoXOOwNS/wDBM79lDTfjh8YNe+IXj63TxRdeHfKjsE1KMTxtdSbmMmxvlAiVV2IBsUybgAyKR9ZTy6lhsM69Xovm2+iPmKmOqYjEKjT6v5WXU9e8beKrjxZpp/s/xFrfhjWIwWs9X0q42z2kmPlZo2zDcxjqYZ0eNv7obDDf/YL/AG8L745eJNc+G3xCttP0n4qeEWZZ2sVZNP8AENsu0rd2ysS0ZKPG7RMSQsisCcukWd+3x4RtfhpqGk+JNPjS0g1mV7a8iT5UM4XesgHYsofdjglQepJPxwPE1x4V/au8LePtNkeK40ma0lmdDgyorvHMhPo9uxjP+y1bUcHRxmGbS1adn1TXT0/4fcipiqmExCi3pfVdGn19f+GLni/w7ceGf2wPFXga+Vo5rW/vDbI3WWI/v4HA/wBqBg3tk+lfWX7AXia1+F/iDV9B1CRbWHXjFLayyHannpuUxk+rqy4zxlMdWAPQft7fsL33x91nQfiD4DutP0r4oeDXVrT7cWWx1y3UsTZ3LKCyAh5FEigkLLIpHzBk5vwj4JuvE2kLJfeHdY8N6lGAl5pepwBZrOT+JRIuYp0ByBNCzxPg4bIICqYyljMKot62Sa6prr6P/gBDC1MLiHJLS90+jT6ev/Dnmn/BVP8Aaw0nxv4/8P8Aw18KXK69qmk3MlxqEdiRMwuivlpbrtPLopkMnZNy5IIYLyPwa/Z8vvEmv+HtHu4/Ovr+5iS6KDcqAtukwe6om7nuEJxX0XoH7PK/bJG0vRoY5rr/AFslvbLGZf8AfcAf+PGvafgv8CbX4byNqFyI5tWmXYCoytsh6qvqx7t+A4yWiWYUsLhlRpbr72318kVHA1MTiHVqbP7kl0P/2Q==
  26. // @require https://code.jquery.com/jquery.min.js
  27. // @require https://unpkg.com/jsmind@0.8.5/es6/jsmind.js
  28. // @require https://unpkg.com/dom-to-image@2.6.0/dist/dom-to-image.min.js
  29. // @require https://unpkg.com/jsmind@0.8.5/es6/jsmind.screenshot.js
  30. // @require https://cdn.jsdelivr.net/npm/@kanety/jquery-simple-tree-table@0.5.1/dist/jquery-simple-tree-table.min.js
  31. // @resource swalStyle https://unpkg.com/jsmind@0.8.5/style/jsmind.css
  32. // ==/UserScript==
  33.  
  34.  
  35. (function() {
  36. 'use strict';
  37.  
  38. const Utils = {
  39. // 检查字符串是否为URL
  40. isUrl: function (string) {
  41. var regexp = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
  42. return regexp.test(string);
  43. },
  44.  
  45. // 检查是否是图片链接
  46. isImg: function (pathImg) {
  47. // var regexp = /^(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?\/([\w#!:.?+=&%@!\-\/])*\.(gif|jpg|jpeg|png|GIF|JPG|PNG)([\w#!:.?+=&%@!\-\/])?/;
  48. let regexp = /\.(ico|bmp|gif|jpg|jpeg|png|svg|webp|GIF|JPG|PNG|WEBP|SVG)([\w#!:.?+=&%@!\-\/])?/i
  49. return regexp.test(pathImg);
  50. },
  51. // 检验内容是否是json格式的内容
  52. isJSON: function (str) {
  53. try {
  54. JSON.parse(str)
  55. return true
  56. } catch(e) {
  57. console.log("is not json", e)
  58. return false
  59. }
  60. },
  61. // 获取数据类型
  62. getType: function (value){
  63. return Object.prototype.toString.call(value).match(/\s(.+)]/)[1].toLowerCase();
  64. },
  65. // JSON 过滤
  66. filterJson: function(json, filter) {
  67. if(!filter){
  68. return json
  69. }
  70.  
  71. filter = filter.toLowerCase()
  72. let newJSON = Utils.getType(json) == 'array' ? [] : {}
  73. for (let key in json) {
  74. let val = json[key]
  75. if (typeof val === 'object') {
  76. let subJSON = this.filterJson(val, filter);
  77. if (Object.keys(subJSON).length > 0) {
  78. newJSON[key] = subJSON;
  79. }
  80. }else{
  81. if(key.toLowerCase().includes(filter.toLowerCase())){
  82. newJSON[key] = val
  83. }
  84.  
  85. if(val !== null && val.toString().toLowerCase().includes(filter.toLowerCase())){
  86. newJSON[key] = val
  87. }
  88. }
  89. }
  90. return newJSON;
  91. }
  92. }
  93.  
  94. // jquery.json-viewer 插件 开始
  95. // 解决和原网页jquery版本冲突
  96. var _jQuery = jQuery.noConflict(true);
  97. (function($){
  98. /**
  99. * 检查 arg 是否为至少包含 1 个元素的数组或至少包含 1 个键的字典
  100. */
  101. function isCollapsable(arg) {
  102. return arg instanceof Object && Object.keys(arg).length > 0;
  103. }
  104.  
  105. /**
  106. * 将 JSON 对象转换为 HTML 表示形式
  107. * @return string
  108. */
  109. function json2html(json, parentPath = '') {
  110. let html = '', type = Utils.getType(json)
  111. switch(type){
  112. case 'array':
  113. case 'object':
  114. let len = json.length || Object.keys(json).length;
  115. if (len > 0) {
  116. html += `<span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">`;
  117. html += type === 'array' ? '[</span><ol class="json-array">' : '{</span><ul class="json-object">';
  118. for (var key in json) {
  119. if (json.hasOwnProperty(key)) {
  120. let comma = --len > 0 ? ',' : '',
  121. jsonPath = parentPath + '.' + key,
  122. collapse = isCollapsable(json[key]) ? '<a href class="json-toggle"></a>' : '',
  123. res = json2html(json[key], jsonPath)
  124. let toHtml = type === 'array' ? res : `<span class="json-key">"${key}"</span>: ${res}`
  125. html += [`<li json-path="${jsonPath}">`, collapse, toHtml, comma, '</li>'].join('')
  126. }
  127. }
  128.  
  129. if(type === 'array'){
  130. html += `</ol><span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">]</span>`
  131. }else{
  132. html += `</ul><span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">}</span>`
  133. }
  134. }else{
  135. html += `<span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">`
  136. html += (type === 'array') ? '[]' : '{}'
  137. html += '</span>'
  138. }
  139. break
  140. default:
  141. /* Escape tags */
  142. json = type === 'string' ? json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : json
  143. if (Utils.isUrl(json)){
  144. html += `<a target="_blank" href="${json}" class="json-string">"${json}"</a>`;
  145. }else{
  146. json = type === 'string' ? `"${json}"` : json
  147. html += `<span class="json-${type}">${json}</span>`;
  148. }
  149. break
  150. }
  151. return html;
  152. }
  153.  
  154. $.fn.jsonViewer = function(json, jsonpFun) {
  155. return this.each(function() {
  156. /* Transform to HTML */
  157. var html = json2html(json);
  158. /** is JSONP */
  159. if(jsonpFun !== undefined && jsonpFun !== null){
  160. html = `<div>${jsonpFun}(</div>${html}<div>)</div>`
  161. }
  162. /* Insert HTML in target DOM element */
  163. $(this).html(html);
  164.  
  165. /* Bind click on toggle buttons */
  166. $(this).off('click');
  167. $(this).on('click', 'a.json-toggle', function() {
  168. var target = $(this).toggleClass('collapsed').siblings('ul.json-object, ol.json-array');
  169. target.toggle();
  170. if (target.is(':visible')) {
  171. target.siblings('.json-placeholder').remove();
  172. }else {
  173. var count = target.children('li:not([class*="hidden"])').length;
  174. var placeholder = count + (count > 1 ? ' items' : ' item');
  175. target.after('<a href class="json-placeholder">' + placeholder + '</a>');
  176. }
  177. return false;
  178. });
  179.  
  180. /* Simulate click on toggle button when placeholder is clicked */
  181. $(this).on('click', 'a.json-placeholder', function() {
  182. $(this).siblings('a.json-toggle').click();
  183. $(this).siblings('a.json-placeholder').remove();
  184. return false;
  185. });
  186. });
  187. };
  188. })(_jQuery);
  189. // jquery.json-viewer 插件 结束
  190.  
  191. (function($){
  192. var source = $('pre').first();
  193. if(source.length === 0){
  194. return
  195. }
  196. let rawText = source.text()
  197. if(!rawText){
  198. return
  199. }
  200.  
  201. // 判断是否为jsonp格式
  202. let jsonpFun = null, tokens = rawText.match(/^([^\s(]*)\s*\(([\s\S]*)\)\s*;?$/)
  203. if (tokens && tokens[1] && tokens[2]) {
  204. jsonpFun = tokens[1]
  205. rawText= tokens[2]
  206. }
  207.  
  208. if(!Utils.isJSON(rawText)){
  209. return
  210. }
  211.  
  212. // 随机rgb颜色
  213. let rgbaColor = `${Math.random()*256}, ${Math.random()*256}, ${Math.random()*256}`
  214. // 添加样式
  215. GM_addStyle(GM_getResourceText('swalStyle'))
  216. $("head").append(`<link rel="stylesheet" type="text/css" href="https://unpkg.com/layer-src@3.5.1/dist/theme/default/layer.css"/>`)
  217. .append('<script src="https://unpkg.com/layui@2.9.17/dist/layui.js" type="text/javascript" ></script>')
  218.  
  219. GM_addStyle(`
  220. body, html{
  221. margin: 0;
  222. padding: 0;
  223. font-size: 14px;
  224. }
  225. td{
  226. font-size: 14px;
  227. }
  228. li::marker {
  229. content: '';
  230. }
  231. .hidden{
  232. display: none !important;
  233. }
  234. .scroll-top{
  235. width: 48px;
  236. height: 48px;
  237. z-index: 999;
  238. position: fixed;
  239. right: 30px;
  240. bottom: 30px;
  241. display: none;
  242. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAA39JREFUaEPtmV2ITVEUx39rJjT5KPKC5DPKK6UGZZ7Eq0whNUiKFPfcKW9mnmjm7ssLaSLygEx5xItMESnKC4V8FV4IJUkyS+fMHZ07zjl3733PvT6a+3S6Z629/r+99sfa+wgpPw1YDtxLe9/k/1eI4X5STMkSokXWNlloYjgpMZSmIxPgbxBfS8M4QK0eavT78Qw0uodrtd+wDGjAMqBcEVAQw6NaYnzeNwRAi2xBOQlMrYj6jLJTygz6iMzyyR1Ai+xCGUgMqnTmDZErgBYoIJjMXhY2S4mLeWUiNwAt0INwyEqY0iNleq1saxjlAqAB64BrToKEHVLijJNPgnHdALqPaUziOsoKRzGfaKFD+nng6FdlXj+Ay9AZq1Q5L2W2/jEA3c8sWqOSe3aCiK/A3cr/K4G2FKEbxXDZF6KuDKROXKWXFvqlxJdQmBaZzDDdKZP8thhW/RmAgKfA4qrgSq+U6UkSlAostEuJOz4Q3hnQgPXAld+CCh1pB5DogKTcSBAalhpHmw1wGtgxJuhbMczJEqIBbxLmzFUxbGg2gCYE9AVADF6jwctJDzKd73xI7DG/IQQTmCFH+OiaBT+AAyykhWcpBZv7JA4bGmaRHOV5cwBqX7mc5Qe9coyX0TK6n/m0RnVSV4bA1KuTLCi/DBTYhHCpRm+Fe8Bo/b8JmJxp71lqOwNowB7guGuqLe33iuGEpW1k5gTgVDK7qIjbZmyESU1aA1TOuA99dTn5CQulxAsbH3uAeqpOGyXVNrvEcMrGzR4gIGnjsonhYzMkhg4bx3EAm17ysGlIBh4DSzzE+LgMiGG3jaPLEApv2Q7YNFq3jcOmZg9gt/u6aH8PzEx0aGWu9PHapjFrgKimCaIDTHiQqf4pNxHW2AT8ZaMEiZdgSreUKdm25QRQgRi7nIZzI7xZcPmeNkAL5xjmVpVQx1049HUGiCAKFBH6K8GjFUP3MIW2CGJpjd4bFEPnmM54grA761tYWpteAJXgy1A6w6p09Opcu1nED7ZlAcQP/BqwGZjORAblMO9sh03czhsgLZgW6EL4JoYL0b0RTArPBfFnH6G5ZyAVYKTkGBlWQVR2bxDDgvjzvwQQXqGsDQ/sGkTXKdHzOECsB3LtjdjKMjqE/qMMjJwn5olh+98+hM6gvAqXy0aN+4Yuo/HG/weAPmC1GNrzHDbxtn4Coc0pQNdM3UAAAAAASUVORK5CYII=)
  243. }
  244. /** 工具栏样式 START **/
  245. .flex-container{
  246. z-index: 10;
  247. position: fixed;
  248. width: 100vw;
  249. height: 100vh;
  250. display: flex;
  251. flex-direction: column;
  252. }
  253. .tabs, .toolbar{
  254. display: flex;
  255. line-height: 28px;
  256. background: #f3f3f3;
  257. border-bottom: 1px solid #e0e0e2;
  258. }
  259. .toolbar{
  260. line-height: 23px;
  261. }
  262. .searchbox{
  263. display: flex;
  264. flex-grow: 1;
  265. }
  266. .toolbar input{
  267. flex-grow: 1;
  268. border: none;
  269. outline: none;
  270. font-size: 12px;
  271. padding-left: 23px;
  272. background-size: 12px;
  273. background-repeat: no-repeat;
  274. background-position: 7px center;
  275. background-image: url(data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIGZpbGw9InJnYmEoMTM1LCAxMzUsIDEzNywgMC45KSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTIgMTIiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgb3BhY2l0eT0iLjQiIGQ9Ik01IDkuMmwyIDEuNlY2LjFMOC41NSA0aC01LjFMNSA2LjF2My4xeiIvPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZD0iTTEuMTggMi42QTEgMSAwIDAgMSAyIDFIMTBhMSAxIDAgMCAxIC44IDEuNkw4IDYuNHY0LjgyYzAgLjYzLS43Mi45OC0xLjIyLjZsLTIuNS0xLjk5QS43NS43NSAwIDAgMSA0IDkuMjVWNi40MUwxLjE4IDIuNnpNMiAyTDUgNi4wOXYzLjA0bDIgMS41OVY2LjA5TDEwLjAxIDJIMnoiLz4KPC9zdmc+Cg==);
  276. }
  277. .clear {
  278. flex: 0 0 auto;
  279. align-self: center;
  280. margin: 0 4px;
  281. padding: 0;
  282. border: 0;
  283. width: 16px;
  284. height: 16px;
  285. background-color: transparent;
  286. background-image: url(data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSI+CiAgPHBhdGggZD0iTTYuNTg2IDhsLTIuMjkzIDIuMjkzYTEgMSAwIDAgMCAxLjQxNCAxLjQxNEw4IDkuNDE0bDIuMjkzIDIuMjkzYTEgMSAwIDAgMCAxLjQxNC0xLjQxNEw5LjQxNCA4bDIuMjkzLTIuMjkzYTEgMSAwIDEgMC0xLjQxNC0xLjQxNEw4IDYuNTg2IDUuNzA3IDQuMjkzYTEgMSAwIDAgMC0xLjQxNCAxLjQxNEw2LjU4NiA4ek04IDBhOCA4IDAgMSAxIDAgMTZBOCA4IDAgMCAxIDggMHoiLz4KPC9zdmc+Cg==);
  287. }
  288. .tabs-item{
  289. border-top: 2px solid #f3f3f3;
  290. }
  291. .tabs-item:hover{
  292. background: #e9e9e9;
  293. border-top: 2px solid #c3c3c6;
  294. }
  295. .tabs-item, .toolbar-item{
  296. cursor: pointer;
  297. padding: 0 10px;
  298. font-size: 12px;
  299. }
  300. .tabs-item.active{
  301. color: #0060df;
  302. border-top: 2px solid #0060df !important;
  303. background: #e9e9e9;
  304. }
  305.  
  306. .tabs-item:hover, .toolbar-item:hover{
  307. background: #e9e9e9;
  308. }
  309. /** 工具栏样式 END **/
  310. /** JSON 格式化样式 START **/
  311. ul.json-object,
  312. ul.json-array {
  313. list-style-type: none;
  314. margin: 0 0 0 2px;
  315. border-left: 1px dotted #5D6D7E;
  316. padding-left: 24px;
  317. }
  318. .json-brackets {
  319. font-weight: 700;
  320. }
  321. .json-key {
  322. /* color: #A31515;*/
  323. color: #910F93;
  324. cursor: pointer;
  325. }
  326. .json-string, .json-string a{
  327. /* color: #0b7500;*/
  328. color: #4B8A4C;
  329. }
  330. .json-number{
  331. /* color: #164FF0;*/
  332. color: #1a01cc;
  333. font-weight: 600;
  334. }
  335. .json-boolean{
  336. color: #905;
  337. font-weight: 600;
  338. }
  339. .json-null {
  340. /* color: #F1592A;*/
  341. color: #0031BC;
  342. font-weight: 600;
  343. }
  344. a.json-toggle {
  345. position: rElative;
  346. color: inherit;
  347. opacity: 0.2;
  348. text-decoration: none;
  349. }
  350. a.json-toggle:hover {
  351. opacity: 0.35;
  352. }
  353. a.json-toggle:active {
  354. opacity: 0.5;
  355. }
  356. a.json-toggle:focus {
  357. outline: none;
  358. }
  359. a.json-toggle:before {
  360. top: 2.5px;
  361. left: -15px;
  362. position: absolute;
  363. content: "";
  364. display: block;
  365. width: 0;
  366. height: 0;
  367. border-style: solid;
  368. border-width: 5px 0 5px 8px;
  369. border-color: transparent transparent transparent currentColor;
  370. transform: rotate(90deg);
  371. }
  372. a.json-toggle.collapsed:before {
  373. transform: rotate(0deg);
  374. }
  375. a.json-placeholder {
  376. color: #aaa;
  377. font-size: 12px;
  378. padding: 0 1em;
  379. text-decoration: none;
  380. }
  381. a.json-placeholder:hover {
  382. text-decoration: underline;
  383. }
  384. .json-curly-brackets {
  385. color: #6D9331;
  386. }
  387. .json-square-brackets{
  388. color: #8E9331;
  389. }
  390. /** 暗黑主题 START **/
  391. body.dark{
  392. background: #333333;
  393. }
  394. .dark li, .dark pre{
  395. color: #CCC;
  396. }
  397. .dark .json-toggle {
  398. opacity: 0.35;
  399. }
  400. .dark .json-toggle:hover {
  401. opacity: 0.5;
  402. }
  403. .dark .json-curly-brackets {
  404. color: #CE70D6;
  405. }
  406. .dark .json-square-brackets{
  407. color: #F1D700;
  408. }
  409. .dark .json-key {
  410. color: #7CDCFE;
  411. }
  412. .dark .json-string, .dark .json-string a{
  413. color: #CE9178;
  414. }
  415. .dark .json-number{
  416. color: #B5CEA8;
  417. }
  418. .dark .json-boolean{
  419. color: #569CD6;
  420. }
  421. .dark .json-null {
  422. color: #2D7AD6;
  423. }
  424. .dark jmnode {
  425. color: #7CDCFE !important;
  426. }
  427. /** 暗黑主题 END **/
  428. /** JSON 格式化样式 END **/
  429. /** 脑图样式 START **/
  430. #jmContainer{
  431. width: 100vw;
  432. height: calc(100vh - 57px);
  433. /* background:#F7F7F7 */
  434. }
  435. jmnode{
  436. display: flex;
  437. align-items: center;
  438. padding: 0 7px 0 22px;
  439. }
  440. jmnode{
  441. color: #475872 !important;
  442. box-shadow: none !important;
  443. background-color: transparent !important;
  444. }
  445. jmnode:hover{
  446. text-shadow: 1px 1px 1px currentColor;
  447. }
  448. jmnode.root {
  449. padding: 0;
  450. color: transparent !important;
  451. }
  452. jmnode:not(.root)::before, jmnode.root::before{
  453. content: " ";
  454. top: 50%;
  455. position: absolute;
  456. border-radius: 50%;
  457. transform: translateY(-50%);
  458. }
  459. jmnode:not(.root)::before{
  460. left: 0;
  461. width: 15px;
  462. height: 15px;
  463. background: rgba(${rgbaColor}, 0.5);
  464. }
  465. jmnode.root::before{
  466. left: 50%;
  467. width: 18px;
  468. height: 18px;
  469. transform: translate(-18px, -50%);
  470. background: rgba(${rgbaColor}, 0.7);
  471. }
  472. jmexpander{
  473. margin-top: 1px;
  474. line-height: 9px;
  475. background: #dfdfdf;
  476. }
  477. .mind-array{
  478. opacity: 0.5;
  479. font-size: 12px;
  480. padding-left: 5px;
  481. }
  482. /** 脑图样式 END **/
  483. /** 容器样式 START **/
  484. .tabs-container{
  485. overflow: auto;
  486. line-height: 1.5;
  487. font-family: monospace;
  488. }
  489. .tabs-container > div{
  490. display: none;
  491. }
  492. .tabs-container > div.active{
  493. display: block;
  494. }
  495. .tabs-container #formatContainer{
  496. padding: 10px;
  497. }
  498. .tabs-container #rawTextContainer{
  499. padding: 0 10px;
  500. }
  501. .tabs-container #rawTextContainer pre{
  502. display: block !important;
  503. overflow-wrap: break-word;
  504. white-space: pre-wrap;
  505. }
  506. /** 容器样式 END **/
  507. .layui-layer-tips{
  508. width: auto !important;
  509. }
  510. .tabs .selectbox{
  511. position: absolute;
  512. right: 200px;
  513. display: flex;
  514. }
  515. /** 表格 START **/
  516. table {
  517. margin-left: 20px;
  518. border-collapse: collapse;
  519. }
  520. table tr:hover{
  521. background: #f0f9fe;
  522. }
  523. .dark table tr:hover{
  524. background: #353B48
  525. }
  526. table tr.selected td, table tr.selected td a{
  527. color: #fff !important;
  528. background-color: #3875d7;
  529. }
  530. table tbody tr td:first-child{
  531. width: 120px;
  532. }
  533. .simple-tree-table-icon{
  534. color: #000;
  535. opacity: 0.2;
  536. width: 0 !important;
  537. margin: 0 !important;
  538. line-height: 0 !important;
  539. }
  540. .simple-tree-table-icon:hover {
  541. opacity: 0.35;
  542. }
  543. .simple-tree-table-icon:active {
  544. opacity: 0.5;
  545. }
  546. .simple-tree-table-icon:after{
  547. content: "" !important;
  548. }
  549. .simple-tree-table-icon:before {
  550. top: 0.5px;
  551. left: -15px;
  552. position: relative;
  553. content: "";
  554. width: 0;
  555. height: 0;
  556. display: none;
  557. border-style: solid;
  558. border-width: 5px 0 5px 8px;
  559. border-color: transparent transparent transparent currentColor;
  560. transform: rotate(90deg);
  561. }
  562. .simple-tree-table-opened .simple-tree-table-icon:before {
  563. display: block;
  564. }
  565. .simple-tree-table-closed .simple-tree-table-icon:before {
  566. display: block;
  567. transform: rotate(0deg);
  568. }
  569. .dark .simple-tree-table-icon{
  570. color: #FFF;
  571. opacity: 0.5;
  572. }
  573. /** 表格 END **/
  574. `)
  575.  
  576. source.hide()
  577. // 将内容用eval函数处理下
  578. const jsonObject = eval(`(${rawText})`);
  579.  
  580. $("body").addClass('dark').append(`
  581. <div class="scroll-top"></div>
  582. <div class="flex-container">
  583. <div class="panel">
  584. <div class="tabs">
  585. <div class="tabs-item btn active" id="format">JSON格式化</div>
  586. <div class="tabs-item btn" id="viewJsonMind">JSON脑图</div>
  587. <div class="tabs-item btn" id="viewRawText">原始数据</div>
  588. <div class="selectbox">
  589. <div class="formatStyle">
  590. <label>风格:</label>
  591. <select>
  592. <option value="default">默认</option>
  593. <option value="treaTable">表格</option>
  594. </select>
  595. </div>
  596. <div class="theme" style="margin-left: 10px">
  597. <label>主题: </label>
  598. <select>
  599. <option value="default">默认</option>
  600. <option value="dark">暗黑</option>
  601. </select>
  602. </div>
  603. </div>
  604. </div>
  605. <div class="toolbar">
  606. <div class="toolbar-item btn" id="saveJson">保存</div>
  607. <div class="toolbar-item btn" id="copyJson">复制</div>
  608. <div class="toolbar-item btn" id="collapseAll">全部折叠</div>
  609. <div class="toolbar-item btn" id="expandAll">全部展开</div>
  610. <div class="toolbar-item btn" id="beautify" style="display: none">美化输出</div>
  611. <div class="searchbox">
  612. <input type="text" placeholder="过滤 JSON "/>
  613. <button class="clear" hidden></button>
  614. </div>
  615. </div>
  616. </div>
  617. <div class="tabs-container">
  618. <div class="active" id="formatContainer"></div>
  619. <div id="jmContainer"></div>
  620. <div id="rawTextContainer"><pre></pre></div>
  621. </div>
  622. </div>`)
  623.  
  624. let btnEvent = {
  625. isFormater: false,
  626. $rawText: $('#rawTextContainer'),
  627. /**
  628. * 保存为文件
  629. */
  630. download: {
  631. download: function(content, filename) {
  632. const link = document.createElement("a")
  633. link.href = content
  634. link.download = filename
  635. link.click()
  636. },
  637. saveJSON: function (text) {
  638. // 创建一个 Blob 对象,包含要下载的文本内容
  639. const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
  640. const url = URL.createObjectURL(blob)
  641. let filename = new Date().getTime() + '.json';
  642. this.download(url, filename)
  643. URL.revokeObjectURL(url);
  644. },
  645. savePNG: () => jm.shoot(),
  646. },
  647. saveJson:function(){
  648. if($('#jmContainer').is(':visible')){
  649. this.download.savePNG()
  650. }else{
  651. this.download.saveJSON(this.$rawText.text())
  652. }
  653. },
  654. // 复制JSON文本内容
  655. copyJson: function(){
  656. GM_setClipboard(this.$rawText.text())
  657. layer.msg('复制成功', {time: 1500})
  658. },
  659. // 全部折叠
  660. collapseAll: function(){
  661. if($('#formatContainer').is(':visible')){
  662. try{
  663. $('.json-toggle').not('.collapsed').click()
  664. }catch(e){}
  665. }else{
  666. jm.collapse_all()
  667. }
  668. },
  669. // 全部展开
  670. expandAll: function(){
  671. if($('#formatContainer').is(':visible')){
  672. try{
  673. $('a.json-placeholder').click().remove()
  674. }catch(e){}
  675. }else{
  676. jm.expand_all()
  677. jm.scroll_node_to_center(jm.get_root())
  678. }
  679. },
  680. format: function(){},
  681. // 显示JSON脑图
  682. viewJsonMind: function(){},
  683. // 查看原始JSON内容
  684. viewRawText: function(){
  685. this.$rawText.html(source.clone())
  686. },
  687. // 美化
  688. beautify: function(){
  689. this.isFormater = !this.isFormater
  690. if(this.isFormater){
  691. this.$rawText.find('pre').text(JSON.stringify(jsonObject, null, 2))
  692. }else{
  693. this.viewRawText()
  694. }
  695. },
  696. init: function(){
  697. this.viewRawText()
  698.  
  699. // 按钮点击事件
  700. $('.btn').click(e => {
  701. const target = e.target, id = target.id
  702. if(target.classList.contains('tabs-item')){
  703. let index = $(target).index()
  704. $(target).addClass('active').siblings().removeClass("active")
  705. $('.tabs-container > div').removeClass("active").eq(index).addClass('active')
  706.  
  707. let beautifyEl = $('#beautify'),
  708. searchEl= $('.searchbox'),
  709. copyJsonEl= $('#copyJson'),
  710. aEl = $('#collapseAll, #expandAll')
  711. id === 'format' ? searchEl.show(): searchEl.hide()
  712. id === 'viewJsonMind' ? copyJsonEl.hide(): copyJsonEl.show()
  713. id === 'viewRawText' ? (beautifyEl.show() && aEl.hide()) : (beautifyEl.hide() && aEl.show())
  714. }
  715. this[id](target)
  716. })
  717.  
  718. return this
  719. }
  720. },
  721. jsonMind = {
  722. // JSON数据转换为jsMind所需要的数据结构
  723. convert: function(json){
  724. let children = []
  725. if(typeof json === 'object'){
  726. for(let key in json){
  727. let val = json[key],
  728. isArray = Array.isArray(val)
  729.  
  730. children.push({
  731. isArray,
  732. chain: key,
  733. id: key + '_' + Math.random(),
  734. topic: isArray ? `${key}<span class="mind-array">[${val.length}]</span>` : `${key}`,
  735. // children: this.convert(val)
  736. children: isArray ? this.convert(val[0]) : this.convert(val)
  737. })
  738. }
  739. }
  740. return children;
  741. },
  742. // 脑图节点调用链
  743. mindChain: function (node){
  744. let chain = node.data.chain
  745. if(!node.parent){
  746. return chain
  747. }
  748.  
  749. let parent = node.parent, parentChain = this.mindChain(parent)
  750. chain = parent.data.isArray ? `${parentChain}[0].${chain}` : `${parentChain}.${chain}`
  751. return chain
  752. },
  753. // 显示脑图
  754. show: function(json, isArr){
  755. jm.show({
  756. "meta":{
  757. "name":"JSON脑图",
  758. "author":"1220301855@qq.com",
  759. "version":"1.0"
  760. },
  761. "format":"node_tree",
  762. /* 数据内容 */
  763. "data": {
  764. "id": "root",
  765. "topic": 'Response',
  766. "direction": "left",
  767. "children": this.convert(json),
  768. "chain": isArr ? 'Response[0]' : 'Response'
  769. }
  770. })
  771.  
  772. setTimeout(() => jm.scroll_node_to_center(jm.get_root()), 300)
  773. return this
  774. },
  775. // 脑图节点事件
  776. event:function(){
  777. $("jmnode").on('dblclick mouseover mouseout', function(event){
  778. let that = $(this), node = jm.get_node(that.attr('nodeid'))
  779. if(!node.parent){
  780. return
  781. }
  782.  
  783. switch(event.type){
  784. case 'dblclick':
  785. GM_setClipboard(jsonMind.mindChain(node))
  786. layer.msg('节点路径复制成功', {time: 1500})
  787. break;
  788. case 'mouseover':
  789. let s = `<b>节点路径(双击复制)</b><br/>${jsonMind.mindChain(node)}`
  790. layer.tips(s, that, {
  791. time: 0,
  792. tips: [2, '#2e59a7']
  793. });
  794. break;
  795. default:
  796. layer.closeAll()
  797. break;
  798. }
  799. })
  800. return this
  801. },
  802. collapseOrExpand: function(){
  803. $("jmnode").on('click', function(){
  804. let node = jm.get_node($(this).attr('nodeid'))
  805. jm.toggle_node(node)
  806. })
  807. return this
  808. },
  809. init: function(json){
  810. let isArr = Array.isArray(json);
  811. if(isArr){
  812. if(typeof json[0] !== 'object'){
  813. layer.msg('数据结构无法生成脑图', {time: 1000})
  814. return
  815. }
  816. json = json[0]
  817. }
  818.  
  819. if(!window.jm){
  820. window.jm = new jsMind({
  821. mode :'side',
  822. editable: false,
  823. container:'jmContainer',
  824. view: {
  825. hmargin: 50, // 思维导图距容器外框的最小水平距离
  826. vmargin: 50, // 思维导图距容器外框的最小垂直距离
  827. engine: 'svg', // 思维导图各节点之间线条的绘制引擎
  828. draggable: true, // 当容器不能完全容纳思维导图时,是否允许拖动画布代替鼠标滚动
  829. support_html : false,
  830. line_color: '#C4C9D0',
  831. },
  832. layout: {
  833. vspace: 7, // 节点之间的垂直间距
  834. hspace: 150, // 节点之间的水平空间
  835. },
  836. });
  837. }
  838.  
  839. this.show(json, isArr).event().collapseOrExpand()
  840. }
  841. },
  842. otherOperate = {
  843. // 过滤 JSON
  844. filterJSON: function(filter) {
  845. let style = GM_getValue('formatStyle') || 'default'
  846. if(!filter){
  847. style == 'default' ? $('#formatContainer li').removeClass('hidden') : $('.json-key').parent().removeClass('hidden')
  848. return
  849. }
  850.  
  851. let chainSet= new Set()
  852. /**
  853. * 模糊匹配 JSON key
  854. * 假如`filter`值为`id`, querySelectorAll得到DOM节点
  855. * 得到:['.feedList.0.images.0.user_id', '.feedList.0.images.0', '.feedList.0.images', '.feedList.0', '.feedList']
  856. */
  857. document.querySelectorAll('#formatContainer *[json-path]').forEach(el => {
  858. let chain = $(el).attr('json-path')
  859. if(!chain){
  860. return
  861. }
  862. let newChain = chain.substr(chain.lastIndexOf('.'))
  863. if(!newChain.toLowerCase().includes(filter.toLowerCase())){
  864. return
  865. }
  866. chainSet.add(chain)
  867. while(chain = chain.substr(0, chain.lastIndexOf('.'))){
  868. chainSet.add(chain)
  869. }
  870. })
  871.  
  872. /**
  873. * 模糊匹配 JSON value
  874. */
  875. document.querySelectorAll("#formatContainer *[class*='json-']:not([class*='json-key']):not([class*='json-brackets'])")
  876. .forEach(el =>{
  877. let target = $(el),
  878. chain = style == 'default' ? target.parent().attr('json-path') : target.siblings('.json-key').attr('json-path')
  879. if(!chain){
  880. return
  881. }
  882. let text = target.text()
  883. if(!text.toLowerCase().includes(filter.toLowerCase())){
  884. return
  885. }
  886. chainSet.add(chain)
  887. while(chain = chain.substr(0, chain.lastIndexOf('.'))){
  888. chainSet.add(chain)
  889. }
  890. })
  891. console.log(chainSet)
  892. style == 'default' ? $('#formatContainer li').addClass('hidden') : $('.json-key').parent().addClass('hidden')
  893. chainSet.forEach(chain => {
  894. style == 'default' ? $(`#formatContainer *[json-path="${chain}"]`).removeClass('hidden')
  895. : $(`#formatContainer *[json-path="${chain}"]`).parent().removeClass('hidden')
  896. })
  897. },
  898. // JSON 过滤
  899. input: function(){
  900. let that = this
  901. $('input').on('input', function(){
  902. let val = $(this).val()
  903. val === '' ? $('.clear').attr('hidden', true) : $('.clear').attr('hidden', false)
  904. that.filterJSON(val)
  905. })
  906. return that
  907. },
  908. // 清空输入框内容
  909. clear: function(){
  910. let that = this
  911. $('.clear').click(function(){
  912. that.filterJSON()
  913. $('input').val('')
  914. $(this).attr('hidden', true)
  915. })
  916. return this
  917. },
  918. // 返回顶部
  919. scrollTop: function(){
  920. $('.scroll-top').click(function(){
  921. $('.tabs-container').animate({
  922. scrollTop: '0'
  923. }, 1000);
  924. })
  925. $('.tabs-container').scroll(function(e) {
  926. let scrollTop = $(this).scrollTop()
  927. scrollTop > 500 ? $('.scroll-top').fadeIn() : $('.scroll-top').fadeOut()
  928. });
  929. return this
  930. },
  931. // 所有a标签,看是否是图片,是图片生成预览图
  932. urlHover:function(){
  933. $("#formatContainer").on({
  934. mouseenter: function(){
  935. var that = $(this),
  936. href = that.attr('href')
  937. if(Utils.isImg(href)){
  938. layer.tips(`<img src="${href}" />`, that, {
  939. time: 0,
  940. anim: 5,
  941. maxWidth: 500,
  942. tips: [3, '#d9d9d9']
  943. });
  944. }
  945. },
  946. mouseleave: () => layer.closeAll()
  947. }, 'a[href]')
  948. return this
  949. },
  950. // 提示key的JSONPath
  951. tipsJsonPath: function(){
  952. var that = this
  953. $("#formatContainer").on({
  954. mouseenter: function(){
  955. let jsonPath = that.getJsonPath(this)
  956. layer.tips(jsonPath, this, {
  957. time: 0,
  958. anim: 5,
  959. maxWidth: 500,
  960. tips: [1, '#2e59a7']
  961. })
  962. },
  963. mouseleave: () => layer.closeAll()
  964. }, '.json-key')
  965. return this
  966. },
  967. // 单击key复制JSONPath
  968. copyJsonPath: function(){
  969. var that = this
  970. $("#formatContainer").on('click', '.json-key', function(event){
  971. if(event.ctrlKey){
  972. let jsonPath = that.getJsonPath(this)
  973. GM_setClipboard(jsonPath)
  974. layer.msg('复制成功', {time: 1500})
  975. }
  976. })
  977. return this
  978. },
  979. getJsonPath: function(element){
  980. let style = GM_getValue('formatStyle') || 'default'
  981. let jsonPath = style == 'default' ? $(element).parent().attr('json-path') : $(element).attr('json-path')
  982. return jsonPath.split('.').reduce((prev, next) => /^\d+$/.test(next) ? prev + `[${next}]` : prev + '.' + next)
  983. },
  984. init:function(){
  985. this.input().clear().scrollTop().urlHover().tipsJsonPath().copyJsonPath()
  986. }
  987. },
  988. theme = {
  989. // 切换主题
  990. changeTheme: function(){
  991. let that = this
  992. $('.theme select').change(function(e){
  993. let val = $(e.target).val()
  994. GM_setValue('theme', val)
  995. that.setTheme()
  996. })
  997. return this
  998. },
  999. // 设置主题
  1000. setTheme: function(){
  1001. let theme = GM_getValue('theme') || 'default'
  1002. $('body').removeClass().addClass(theme)
  1003. $('.theme select').val(theme)
  1004. return this
  1005. },
  1006. init:function(){
  1007. this.setTheme().changeTheme()
  1008. }
  1009. },
  1010. formatStyle = {
  1011. // 切换风格
  1012. changeStyle: function(json){
  1013. let that = this
  1014. $('.formatStyle select').change(function(e){
  1015. let val = $(e.target).val()
  1016. GM_setValue('formatStyle', val)
  1017. that.setStyle(json)
  1018. })
  1019. return this
  1020. },
  1021. // 设置风格
  1022. setStyle: function(json){
  1023. let style = GM_getValue('formatStyle') || 'default'
  1024. $('.formatStyle select').val(style)
  1025.  
  1026. $('input').val('')
  1027. $('#formatContainer').html('')
  1028. if(style === 'default'){
  1029. $('#formatContainer').jsonViewer(json, jsonpFun)
  1030. }else{
  1031. let trHtml= treeTableHtml(json)
  1032. $('#formatContainer').append(`<table id="treeTable">${trHtml}</table>`)
  1033. $('#treeTable').simpleTreeTable({
  1034. expander: '#expandAll',
  1035. collapser: '#collapseAll',
  1036. })
  1037.  
  1038. // Highlight selected row
  1039. $('#treeTable').on("mousedown", "tr", function() {
  1040. $(".selected").not(this).removeClass("selected");
  1041. $(this).toggleClass("selected")
  1042. })
  1043. }
  1044. return this
  1045. },
  1046. init:function(json){
  1047. this.setStyle(json).changeStyle(json)
  1048. theme.init()
  1049. btnEvent.init()
  1050. otherOperate.init()
  1051. jsonMind.init(json)
  1052. }
  1053. }
  1054.  
  1055. formatStyle.init(jsonObject)
  1056.  
  1057. /**
  1058. * 表格
  1059. */
  1060. function treeTableHtml(json, level = 0, pId = '', pChain = ''){
  1061. let tr = ''
  1062. for(let key in json){
  1063. let val = json[key],
  1064. type = Utils.getType(val),
  1065. tId = key + '_' + Math.random(),
  1066. chain = pChain + "." + key
  1067. if(['array', 'object'].includes(type)){
  1068. let brackets = '',
  1069. len = val.length,
  1070. res = treeTableHtml(val, level + 1, tId, chain)
  1071.  
  1072. if(!res){
  1073. if(type ==='array'){
  1074. brackets = `<span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">[]</span>`
  1075. }else{
  1076. brackets = `<span class="json-brackets ${type == 'array' ? 'json-square-brackets' : 'json-curly-brackets'}">{}</span>`
  1077. }
  1078. }
  1079.  
  1080. tr += `
  1081. <tr data-node-id="${tId}" data-node-pid="${pId}" type="${type}">
  1082. <td colspan="${len > 0 ? 2 : 0}" class="json-key" json-path="${chain}" style="padding-left: ${ level * 19 }px">${key}:</td>
  1083. <td>${brackets}</td>
  1084. </tr>`
  1085. tr += res
  1086. }else{
  1087. val = (type === 'string') ? val.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') : val
  1088. tr += `<tr data-node-id="${tId}" data-node-pid="${pId}" type="${type}">
  1089. <td class="json-key" json-path="${chain}" style="padding-left: ${ level * 19 }px">${key}:</td>`
  1090. if (Utils.isUrl(val)){
  1091. tr += `<td class="json-${type}"><a target="_blank" href="${val}">"${val}"</a></td>`
  1092. }else{
  1093. val = (type === 'string') ? `"${val}"`: val
  1094. tr += `<td class="json-${type}">${val}</td>`
  1095. }
  1096. tr += '</tr>'
  1097. }
  1098. }
  1099. return tr;
  1100. }
  1101.  
  1102. })(_jQuery)
  1103. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址