Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
03b3117
1
Parent(s): 2d8c38a
优化
Browse files- static/css/prism.css +1 -0
- static/js/prism-json.js +1 -0
- static/js/prism.js +1 -0
- static/js/tailwind.js +0 -0
- static/js/vue.js +0 -0
- templates/index.html +435 -265
static/css/prism.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
static/js/prism-json.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json;
|
static/js/prism.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(l){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,e={},j={manual:l.Prism&&l.Prism.manual,disableWorkerMessageHandler:l.Prism&&l.Prism.disableWorkerMessageHandler,util:{encode:function e(t){return t instanceof C?new C(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function n(e,a){var r,t;switch(a=a||{},j.util.type(e)){case"Object":if(t=j.util.objId(e),a[t])return a[t];for(var s in r={},a[t]=r,e)e.hasOwnProperty(s)&&(r[s]=n(e[s],a));return r;case"Array":return(t=j.util.objId(e),a[t])?a[t]:(r=[],a[t]=r,e.forEach(function(e,t){r[t]=n(e,a)}),r);default:return e}},getLanguage:function(e){for(;e;){var t=n.exec(e.className);if(t)return t[1].toLowerCase();e=e.parentElement}return"none"},setLanguage:function(e,t){e.className=e.className.replace(RegExp(n,"gi"),""),e.classList.add("language-"+t)},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(e){var t=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(e.stack)||[])[1];if(t){var n,a=document.getElementsByTagName("script");for(n in a)if(a[n].src==t)return a[n]}return null}},isActive:function(e,t,n){for(var a="no-"+t;e;){var r=e.classList;if(r.contains(t))return!0;if(r.contains(a))return!1;e=e.parentElement}return!!n}},languages:{plain:e,plaintext:e,text:e,txt:e,extend:function(e,t){var n,a=j.util.clone(j.languages[e]);for(n in t)a[n]=t[n];return a},insertBefore:function(n,e,t,a){var r,s=(a=a||j.languages)[n],i={};for(r in s)if(s.hasOwnProperty(r)){if(r==e)for(var o in t)t.hasOwnProperty(o)&&(i[o]=t[o]);t.hasOwnProperty(r)||(i[r]=s[r])}var l=a[n];return a[n]=i,j.languages.DFS(j.languages,function(e,t){t===l&&e!=n&&(this[e]=i)}),i},DFS:function e(t,n,a,r){r=r||{};var s,i,o,l=j.util.objId;for(s in t)t.hasOwnProperty(s)&&(n.call(t,s,t[s],a||s),i=t[s],"Object"!==(o=j.util.type(i))||r[l(i)]?"Array"!==o||r[l(i)]||(r[l(i)]=!0,e(i,n,s,r)):(r[l(i)]=!0,e(i,n,null,r)))}},plugins:{},highlightAll:function(e,t){j.highlightAllUnder(document,e,t)},highlightAllUnder:function(e,t,n){var a={callback:n,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};j.hooks.run("before-highlightall",a),a.elements=Array.prototype.slice.apply(a.container.querySelectorAll(a.selector)),j.hooks.run("before-all-elements-highlight",a);for(var r,s=0;r=a.elements[s++];)j.highlightElement(r,!0===t,a.callback)},highlightElement:function(e,t,n){var a=j.util.getLanguage(e),r=j.languages[a],s=(j.util.setLanguage(e,a),e.parentElement);s&&"pre"===s.nodeName.toLowerCase()&&j.util.setLanguage(s,a);var i={element:e,language:a,grammar:r,code:e.textContent};function o(e){i.highlightedCode=e,j.hooks.run("before-insert",i),i.element.innerHTML=i.highlightedCode,j.hooks.run("after-highlight",i),j.hooks.run("complete",i),n&&n.call(i.element)}if(j.hooks.run("before-sanity-check",i),(s=i.element.parentElement)&&"pre"===s.nodeName.toLowerCase()&&!s.hasAttribute("tabindex")&&s.setAttribute("tabindex","0"),!i.code)return j.hooks.run("complete",i),void(n&&n.call(i.element));j.hooks.run("before-highlight",i),i.grammar?t&&l.Worker?((a=new Worker(j.filename)).onmessage=function(e){o(e.data)},a.postMessage(JSON.stringify({language:i.language,code:i.code,immediateClose:!0}))):o(j.highlight(i.code,i.grammar,i.language)):o(j.util.encode(i.code))},highlight:function(e,t,n){e={code:e,grammar:t,language:n};if(j.hooks.run("before-tokenize",e),e.grammar)return e.tokens=j.tokenize(e.code,e.grammar),j.hooks.run("after-tokenize",e),C.stringify(j.util.encode(e.tokens),e.language);throw new Error('The language "'+e.language+'" has no grammar.')},tokenize:function(e,t){var n=t.rest;if(n){for(var a in n)t[a]=n[a];delete t.rest}for(var r=new u,s=(z(r,r.head,e),!function e(t,n,a,r,s,i){for(var o in a)if(a.hasOwnProperty(o)&&a[o]){var l=a[o];l=Array.isArray(l)?l:[l];for(var u=0;u<l.length;++u){if(i&&i.cause==o+","+u)return;for(var g,c=l[u],d=c.inside,p=!!c.lookbehind,m=!!c.greedy,h=c.alias,f=(m&&!c.pattern.global&&(g=c.pattern.toString().match(/[imsuy]*$/)[0],c.pattern=RegExp(c.pattern.source,g+"g")),c.pattern||c),b=r.next,y=s;b!==n.tail&&!(i&&y>=i.reach);y+=b.value.length,b=b.next){var v=b.value;if(n.length>t.length)return;if(!(v instanceof C)){var F,x=1;if(m){if(!(F=L(f,y,t,p))||F.index>=t.length)break;var k=F.index,w=F.index+F[0].length,A=y;for(A+=b.value.length;A<=k;)b=b.next,A+=b.value.length;if(A-=b.value.length,y=A,b.value instanceof C)continue;for(var P=b;P!==n.tail&&(A<w||"string"==typeof P.value);P=P.next)x++,A+=P.value.length;x--,v=t.slice(y,A),F.index-=y}else if(!(F=L(f,0,v,p)))continue;var k=F.index,$=F[0],S=v.slice(0,k),E=v.slice(k+$.length),v=y+v.length,_=(i&&v>i.reach&&(i.reach=v),b.prev),S=(S&&(_=z(n,_,S),y+=S.length),O(n,_,x),new C(o,d?j.tokenize($,d):$,h,$));b=z(n,_,S),E&&z(n,b,E),1<x&&($={cause:o+","+u,reach:v},e(t,n,a,b.prev,y,$),i&&$.reach>i.reach&&(i.reach=$.reach))}}}}}(e,r,t,r.head,0),r),i=[],o=s.head.next;o!==s.tail;)i.push(o.value),o=o.next;return i},hooks:{all:{},add:function(e,t){var n=j.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=j.hooks.all[e];if(n&&n.length)for(var a,r=0;a=n[r++];)a(t)}},Token:C};function C(e,t,n,a){this.type=e,this.content=t,this.alias=n,this.length=0|(a||"").length}function L(e,t,n,a){e.lastIndex=t;t=e.exec(n);return t&&a&&t[1]&&(e=t[1].length,t.index+=e,t[0]=t[0].slice(e)),t}function u(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function z(e,t,n){var a=t.next,n={value:n,prev:t,next:a};return t.next=n,a.prev=n,e.length++,n}function O(e,t,n){for(var a=t.next,r=0;r<n&&a!==e.tail;r++)a=a.next;(t.next=a).prev=t,e.length-=r}if(l.Prism=j,C.stringify=function t(e,n){if("string"==typeof e)return e;var a;if(Array.isArray(e))return a="",e.forEach(function(e){a+=t(e,n)}),a;var r,s={type:e.type,content:t(e.content,n),tag:"span",classes:["token",e.type],attributes:{},language:n},e=e.alias,i=(e&&(Array.isArray(e)?Array.prototype.push.apply(s.classes,e):s.classes.push(e)),j.hooks.run("wrap",s),"");for(r in s.attributes)i+=" "+r+'="'+(s.attributes[r]||"").replace(/"/g,""")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'"'+i+">"+s.content+"</"+s.tag+">"},!l.document)return l.addEventListener&&(j.disableWorkerMessageHandler||l.addEventListener("message",function(e){var e=JSON.parse(e.data),t=e.language,n=e.code,e=e.immediateClose;l.postMessage(j.highlight(n,j.languages[t],t)),e&&l.close()},!1)),j;var a,e=j.util.currentScript();function r(){j.manual||j.highlightAll()}return e&&(j.filename=e.src,e.hasAttribute("data-manual")&&(j.manual=!0)),j.manual||("loading"===(a=document.readyState)||"interactive"===a&&e&&e.defer?document.addEventListener("DOMContentLoaded",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)),j}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={},n=(n["language-"+t]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^<!\[CDATA\[|\]\]>$/i,{"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:n}}),t=(n["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]},{});t[e]={pattern:RegExp(/(<__[^>]*>)(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?(?=<\/__>)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",t)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/,t=(e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css,e.languages.markup);t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism),Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),Prism.languages.js=Prism.languages.javascript,function(){var l,u,g,c,e;void 0!==Prism&&"undefined"!=typeof document&&(Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},c="pre[data-src]:not(["+(u="data-src-status")+'="loaded"]):not(['+u+'="'+(g="loading")+'"])',Prism.hooks.add("before-highlightall",function(e){e.selector+=", "+c}),Prism.hooks.add("before-sanity-check",function(e){var r,t,n,a,s,i,o=e.element;o.matches(c)&&(e.code="",o.setAttribute(u,g),(r=o.appendChild(document.createElement("CODE"))).textContent="Loading…",t=o.getAttribute("data-src"),"none"===(e=e.language)&&(n=(/\.(\w+)$/.exec(t)||[,"none"])[1],e=l[n]||n),Prism.util.setLanguage(r,e),Prism.util.setLanguage(o,e),(n=Prism.plugins.autoloader)&&n.loadLanguages(e),n=t,a=function(e){o.setAttribute(u,"loaded");var t,n,a=function(e){var t,n;if(e=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(e||""))return t=Number(e[1]),n=e[2],e=e[3],n?e?[t,Number(e)]:[t,void 0]:[t,t]}(o.getAttribute("data-range"));a&&(t=e.split(/\r\n?|\n/g),n=a[0],a=null==a[1]?t.length:a[1],n<0&&(n+=t.length),n=Math.max(0,Math.min(n-1,t.length)),a<0&&(a+=t.length),a=Math.max(0,Math.min(a,t.length)),e=t.slice(n,a).join("\n"),o.hasAttribute("data-start")||o.setAttribute("data-start",String(n+1))),r.textContent=e,Prism.highlightElement(r)},s=function(e){o.setAttribute(u,"failed"),r.textContent=e},(i=new XMLHttpRequest).open("GET",n,!0),i.onreadystatechange=function(){4==i.readyState&&(i.status<400&&i.responseText?a(i.responseText):400<=i.status?s("✖ Error "+i.status+" while fetching file: "+i.statusText):s("✖ Error: File does not exist or is empty"))},i.send(null))}),e=!(Prism.plugins.fileHighlight={highlight:function(e){for(var t,n=(e||document).querySelectorAll(c),a=0;t=n[a++];)Prism.highlightElement(t)}}),Prism.fileHighlight=function(){e||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),e=!0),Prism.plugins.fileHighlight.highlight.apply(this,arguments)})}();
|
static/js/tailwind.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/vue.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
templates/index.html
CHANGED
|
@@ -4,13 +4,12 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>HTTP 接口调试工作室 | HTTP Request Studio</title>
|
| 7 |
-
<
|
| 8 |
-
<script src="
|
| 9 |
-
<
|
| 10 |
-
<
|
| 11 |
-
<
|
| 12 |
-
<script src="
|
| 13 |
-
<script src="https://lib.baomitu.com/prism/1.29.0/components/prism-json.min.js"></script>
|
| 14 |
<style>
|
| 15 |
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
| 16 |
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
|
@@ -26,77 +25,130 @@
|
|
| 26 |
font-family: 'Fira Code', monospace;
|
| 27 |
tab-size: 2;
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</style>
|
| 30 |
</head>
|
| 31 |
-
<body class="bg-slate-900 text-slate-200 h-screen overflow-hidden flex">
|
| 32 |
|
| 33 |
<div id="app" class="w-full h-full flex flex-col">
|
| 34 |
<!-- Header -->
|
| 35 |
-
<header class="h-
|
| 36 |
-
<div class="flex items-center space-x-
|
| 37 |
-
<div class="bg-blue-600 w-
|
| 38 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
-
<h1 class="font-bold text-lg text-white tracking-wide">HTTP Request Studio</h1>
|
| 41 |
</div>
|
| 42 |
-
<div class="flex items-center space-x-4 text-sm
|
|
|
|
| 43 |
<div class="relative group">
|
| 44 |
-
<button class="hover:text-white transition flex items-center">
|
| 45 |
-
<
|
|
|
|
|
|
|
|
|
|
| 46 |
</button>
|
| 47 |
-
<div class="absolute right-0 mt-2 w-48 bg-slate-800 border border-slate-700 rounded shadow-xl hidden group-hover:block z-50">
|
| 48 |
-
<a href="#" @click.prevent="loadDemo('get')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300">GET JSON</a>
|
| 49 |
-
<a href="#" @click.prevent="loadDemo('post')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300">POST JSON</a>
|
| 50 |
-
<a href="#" @click.prevent="loadDemo('image')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300">GET Image</a>
|
| 51 |
-
<a href="#" @click.prevent="loadDemo('404')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300">404 Error</a>
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</span>
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
</div>
|
| 59 |
</header>
|
| 60 |
|
| 61 |
<!-- Main Content -->
|
| 62 |
<div class="flex-1 flex overflow-hidden">
|
| 63 |
<!-- Sidebar (History) -->
|
| 64 |
-
<div class="w-64 bg-slate-
|
| 65 |
-
<div class="p-3 border-b border-slate-
|
| 66 |
-
<span class="font-semibold text-slate-
|
| 67 |
-
<button @click="clearHistory" class="text-xs text-slate-500 hover:text-red-400" title="清空历史">
|
| 68 |
-
<
|
|
|
|
|
|
|
| 69 |
</button>
|
| 70 |
</div>
|
| 71 |
-
<div class="flex-1 overflow-y-auto p-2 space-y-2">
|
| 72 |
-
<div v-if="history.length === 0" class="text-center text-slate-
|
|
|
|
|
|
|
|
|
|
| 73 |
暂无记录
|
| 74 |
</div>
|
| 75 |
<div v-for="(item, index) in history" :key="index"
|
| 76 |
@click="loadHistory(item)"
|
| 77 |
-
class="group p-
|
| 78 |
<div class="flex items-center justify-between mb-1">
|
| 79 |
-
<span :class="getMethodColor(item.method)" class="text-
|
| 80 |
-
<span class="text-
|
| 81 |
</div>
|
| 82 |
-
<div class="text-xs text-slate-300 truncate font-mono" :title="item.url">${ item.url }</div>
|
| 83 |
-
<div class="flex items-center
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
<!-- Workspace -->
|
| 94 |
-
<div class="flex-1 flex flex-col min-w-0 bg-slate-900">
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
<!-- Request Bar -->
|
| 97 |
-
<div class="p-4 border-b border-slate-
|
| 98 |
<div class="flex space-x-2">
|
| 99 |
-
<select v-model="currentRequest.method" class="bg-slate-800 border border-slate-600 text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-blue-500 font-bold w-28">
|
| 100 |
<option value="GET">GET</option>
|
| 101 |
<option value="POST">POST</option>
|
| 102 |
<option value="PUT">PUT</option>
|
|
@@ -105,16 +157,26 @@
|
|
| 105 |
<option value="HEAD">HEAD</option>
|
| 106 |
<option value="OPTIONS">OPTIONS</option>
|
| 107 |
</select>
|
| 108 |
-
<div class="flex-1 relative">
|
| 109 |
<input type="text" v-model="currentRequest.url"
|
| 110 |
placeholder="输入 URL (例如: https://httpbin.org/get)"
|
| 111 |
@keyup.enter="sendRequest"
|
| 112 |
-
class="w-full bg-slate-800 border border-slate-600 text-white text-sm rounded px-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
</div>
|
| 114 |
<button @click="sendRequest" :disabled="loading"
|
| 115 |
-
class="bg-blue-600 hover:bg-blue-500 text-white px-
|
| 116 |
-
<
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
发送
|
| 119 |
</button>
|
| 120 |
</div>
|
|
@@ -124,16 +186,16 @@
|
|
| 124 |
<div class="flex-1 flex flex-col md:flex-row overflow-hidden">
|
| 125 |
|
| 126 |
<!-- Request Config (Left/Top) -->
|
| 127 |
-
<div class="flex-1 flex flex-col border-r border-slate-
|
| 128 |
<!-- Tabs -->
|
| 129 |
-
<div class="flex border-b border-slate-
|
| 130 |
<button v-for="tab in ['Params', 'Headers', 'Body', 'Auth']"
|
| 131 |
@click="activeReqTab = tab"
|
| 132 |
-
:class="activeReqTab === tab ? 'text-blue-400 border-b-2 border-blue-400 bg-slate-800' : 'text-slate-400 hover:text-slate-200'"
|
| 133 |
-
class="px-
|
| 134 |
${ tab }
|
| 135 |
-
<span v-if="tab === 'Params' && paramsCount > 0" class="ml-
|
| 136 |
-
<span v-if="tab === 'Headers' && headersCount > 0" class="ml-
|
| 137 |
</button>
|
| 138 |
</div>
|
| 139 |
|
|
@@ -141,105 +203,136 @@
|
|
| 141 |
<div class="flex-1 overflow-y-auto p-4 bg-slate-900">
|
| 142 |
|
| 143 |
<!-- Params Tab -->
|
| 144 |
-
<div v-if="activeReqTab === 'Params'">
|
| 145 |
-
<div class="space-y-
|
| 146 |
-
<div class="
|
| 147 |
-
|
| 148 |
-
<
|
| 149 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
</div>
|
| 151 |
-
<input type="text" v-model="param.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-
|
| 152 |
-
<input type="text" v-model="param.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-
|
| 153 |
-
<button @click="removeParam(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2">
|
| 154 |
-
<
|
|
|
|
|
|
|
| 155 |
</button>
|
| 156 |
</div>
|
| 157 |
-
<button @click="addParam" class="mt-2 text-xs text-blue-400 hover:text-blue-300 flex items-center">
|
| 158 |
-
<i class="fa-solid fa-plus mr-1"></i> 添加参数
|
| 159 |
-
</button>
|
| 160 |
</div>
|
| 161 |
</div>
|
| 162 |
|
| 163 |
<!-- Headers Tab -->
|
| 164 |
-
<div v-if="activeReqTab === 'Headers'">
|
| 165 |
-
<div class="space-y-
|
| 166 |
-
<div class="
|
| 167 |
-
|
| 168 |
-
<
|
| 169 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
</div>
|
| 171 |
-
<input type="text" v-model="header.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-
|
| 172 |
-
<input type="text" v-model="header.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-
|
| 173 |
-
<button @click="removeHeader(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2">
|
| 174 |
-
<
|
|
|
|
|
|
|
| 175 |
</button>
|
| 176 |
</div>
|
| 177 |
-
<button @click="addHeader" class="mt-2 text-xs text-blue-400 hover:text-blue-300 flex items-center">
|
| 178 |
-
<i class="fa-solid fa-plus mr-1"></i> 添加 Header
|
| 179 |
-
</button>
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
|
| 183 |
<!-- Body Tab -->
|
| 184 |
-
<div v-if="activeReqTab === 'Body'">
|
| 185 |
-
<div class="flex items-center space-x-
|
| 186 |
-
<label class="flex items-center space-x-2 cursor-pointer">
|
| 187 |
-
<input type="radio" v-model="currentRequest.bodyType" value="none" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600">
|
| 188 |
-
<span class="text-sm text-slate-
|
| 189 |
</label>
|
| 190 |
-
<label class="flex items-center space-x-2 cursor-pointer">
|
| 191 |
-
<input type="radio" v-model="currentRequest.bodyType" value="json" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600">
|
| 192 |
-
<span class="text-sm text-slate-
|
| 193 |
</label>
|
| 194 |
-
<label class="flex items-center space-x-2 cursor-pointer">
|
| 195 |
-
<input type="radio" v-model="currentRequest.bodyType" value="text" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600">
|
| 196 |
-
<span class="text-sm text-slate-
|
| 197 |
</label>
|
| 198 |
</div>
|
| 199 |
-
<div v-if="currentRequest.bodyType === 'json'" class="
|
| 200 |
-
<button @click="formatBody" class="absolute top-2 right-2 z-10 text-xs bg-slate-
|
| 201 |
-
|
| 202 |
</button>
|
| 203 |
<textarea v-model="currentRequest.bodyContent"
|
| 204 |
-
class="w-full h-full bg-slate-800
|
| 205 |
placeholder="{ 'key': 'value' }"></textarea>
|
| 206 |
</div>
|
| 207 |
-
<div v-if="currentRequest.bodyType === 'text'" class="
|
| 208 |
<textarea v-model="currentRequest.bodyContent"
|
| 209 |
-
class="w-full h-full bg-slate-800
|
| 210 |
placeholder="Raw text content..."></textarea>
|
| 211 |
</div>
|
| 212 |
-
<div v-if="currentRequest.bodyType === 'none'" class="
|
| 213 |
-
|
|
|
|
| 214 |
</div>
|
| 215 |
</div>
|
| 216 |
|
| 217 |
<!-- Auth Tab (Simple) -->
|
| 218 |
-
<div v-if="activeReqTab === 'Auth'">
|
| 219 |
-
<div class="
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
| 223 |
</div>
|
| 224 |
|
| 225 |
</div>
|
| 226 |
</div>
|
| 227 |
|
| 228 |
<!-- Response (Right/Bottom) -->
|
| 229 |
-
<div class="flex-1 flex flex-col min-w-[300px] bg-slate-900 border-l border-slate-
|
| 230 |
<div v-if="response" class="h-full flex flex-col">
|
| 231 |
<!-- Response Meta -->
|
| 232 |
-
<div class="px-4 py-
|
| 233 |
-
<div class="flex items-center space-x-
|
| 234 |
-
<span class="text-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
<
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
</div>
|
| 240 |
-
<div>
|
| 241 |
-
|
| 242 |
-
<
|
|
|
|
|
|
|
|
|
|
| 243 |
</button>
|
| 244 |
</div>
|
| 245 |
</div>
|
|
@@ -248,26 +341,35 @@
|
|
| 248 |
<div class="flex-1 overflow-hidden relative group bg-slate-900">
|
| 249 |
<div v-if="response.is_binary" class="w-full h-full flex flex-col items-center justify-center p-4 overflow-auto">
|
| 250 |
<div v-if="response.content_type && response.content_type.startsWith('image/')" class="text-center">
|
| 251 |
-
<img :src="`data:${response.content_type};base64,${response.data}`" class="max-w-full max-h-[
|
| 252 |
-
<div class="mt-
|
| 253 |
</div>
|
| 254 |
<div v-else class="text-center">
|
| 255 |
-
<
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
</div>
|
| 260 |
</div>
|
| 261 |
-
<div v-else class="w-full h-full">
|
| 262 |
-
<pre v-if="response.is_json" class="w-full h-full p-4 overflow-auto text-sm font-mono language-json"><code class="language-json">${ formatResponseData(response.data) }</code></pre>
|
| 263 |
-
<pre v-else class="w-full h-full p-4 overflow-auto text-sm font-mono text-slate-300 whitespace-pre-wrap">${ response.data }</pre>
|
| 264 |
</div>
|
| 265 |
</div>
|
| 266 |
</div>
|
| 267 |
|
| 268 |
<div v-else class="flex-1 flex flex-col items-center justify-center text-slate-600">
|
| 269 |
-
<
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
</div>
|
| 272 |
</div>
|
| 273 |
|
|
@@ -276,6 +378,18 @@
|
|
| 276 |
</div>
|
| 277 |
</div>
|
| 278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
<script>
|
| 280 |
const { createApp, ref, computed, onMounted, nextTick } = Vue;
|
| 281 |
|
|
@@ -286,6 +400,9 @@
|
|
| 286 |
const activeReqTab = ref('Params');
|
| 287 |
const history = ref([]);
|
| 288 |
const response = ref(null);
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
const currentRequest = ref({
|
| 291 |
method: 'GET',
|
|
@@ -306,7 +423,7 @@
|
|
| 306 |
history.value = JSON.parse(saved);
|
| 307 |
}
|
| 308 |
|
| 309 |
-
// Add default header
|
| 310 |
if (currentRequest.value.headers.length === 0 || (currentRequest.value.headers.length === 1 && !currentRequest.value.headers[0].key)) {
|
| 311 |
currentRequest.value.headers = [
|
| 312 |
{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true },
|
|
@@ -329,6 +446,13 @@
|
|
| 329 |
activeReqTab.value = 'Headers';
|
| 330 |
};
|
| 331 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
const formatBody = () => {
|
| 333 |
try {
|
| 334 |
const obj = JSON.parse(currentRequest.value.bodyContent);
|
|
@@ -357,11 +481,11 @@
|
|
| 357 |
};
|
| 358 |
|
| 359 |
const getStatusColor = (status) => {
|
| 360 |
-
if (status >= 200 && status < 300) return 'text-green-400 bg-green-900';
|
| 361 |
-
if (status >= 300 && status < 400) return 'text-yellow-400 bg-yellow-900';
|
| 362 |
-
if (status >= 400 && status < 500) return 'text-orange-400 bg-orange-900';
|
| 363 |
-
if (status >= 500) return 'text-red-400 bg-red-900';
|
| 364 |
-
return 'text-slate-400';
|
| 365 |
};
|
| 366 |
|
| 367 |
const formatSize = (bytes) => {
|
|
@@ -375,124 +499,6 @@
|
|
| 375 |
return new Date(ts).toLocaleTimeString();
|
| 376 |
};
|
| 377 |
|
| 378 |
-
const sendRequest = async () => {
|
| 379 |
-
if (!currentRequest.value.url) {
|
| 380 |
-
alert('请输入 URL');
|
| 381 |
-
return;
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
loading.value = true;
|
| 385 |
-
response.value = null;
|
| 386 |
-
|
| 387 |
-
// Prepare payload
|
| 388 |
-
const headers = {};
|
| 389 |
-
currentRequest.value.headers.forEach(h => {
|
| 390 |
-
if (h.active && h.key) headers[h.key] = h.value;
|
| 391 |
-
});
|
| 392 |
-
|
| 393 |
-
const params = {};
|
| 394 |
-
currentRequest.value.params.forEach(p => {
|
| 395 |
-
if (p.active && p.key) params[p.key] = p.value;
|
| 396 |
-
});
|
| 397 |
-
|
| 398 |
-
// Set Content-Type if JSON body
|
| 399 |
-
if (currentRequest.value.bodyType === 'json') {
|
| 400 |
-
if (!headers['Content-Type']) {
|
| 401 |
-
headers['Content-Type'] = 'application/json';
|
| 402 |
-
}
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
const payload = {
|
| 406 |
-
method: currentRequest.value.method,
|
| 407 |
-
url: currentRequest.value.url,
|
| 408 |
-
headers: headers,
|
| 409 |
-
params: params,
|
| 410 |
-
body: currentRequest.value.bodyType !== 'none' ? currentRequest.value.bodyContent : null
|
| 411 |
-
};
|
| 412 |
-
|
| 413 |
-
try {
|
| 414 |
-
const res = await fetch('/api/proxy', {
|
| 415 |
-
method: 'POST',
|
| 416 |
-
headers: { 'Content-Type': 'application/json' },
|
| 417 |
-
body: JSON.stringify(payload)
|
| 418 |
-
});
|
| 419 |
-
|
| 420 |
-
const data = await res.json();
|
| 421 |
-
response.value = data;
|
| 422 |
-
|
| 423 |
-
// Add to history
|
| 424 |
-
addToHistory({
|
| 425 |
-
...payload,
|
| 426 |
-
timestamp: Date.now(),
|
| 427 |
-
status: data.status,
|
| 428 |
-
duration: data.duration
|
| 429 |
-
});
|
| 430 |
-
|
| 431 |
-
// Highlight syntax
|
| 432 |
-
nextTick(() => {
|
| 433 |
-
Prism.highlightAll();
|
| 434 |
-
});
|
| 435 |
-
|
| 436 |
-
} catch (err) {
|
| 437 |
-
response.value = {
|
| 438 |
-
status: 0,
|
| 439 |
-
status_text: 'Network Error',
|
| 440 |
-
data: err.message,
|
| 441 |
-
duration: 0,
|
| 442 |
-
size: 0,
|
| 443 |
-
is_json: false
|
| 444 |
-
};
|
| 445 |
-
} finally {
|
| 446 |
-
loading.value = false;
|
| 447 |
-
}
|
| 448 |
-
};
|
| 449 |
-
|
| 450 |
-
const addToHistory = (item) => {
|
| 451 |
-
// Remove duplicate if exists (same method and url) - optional
|
| 452 |
-
// For now, just prepend
|
| 453 |
-
history.value.unshift(item);
|
| 454 |
-
if (history.value.length > 50) history.value.pop();
|
| 455 |
-
localStorage.setItem('http-studio-history', JSON.stringify(history.value));
|
| 456 |
-
};
|
| 457 |
-
|
| 458 |
-
const clearHistory = () => {
|
| 459 |
-
if(confirm('确定清空历史记录吗?')) {
|
| 460 |
-
history.value = [];
|
| 461 |
-
localStorage.removeItem('http-studio-history');
|
| 462 |
-
}
|
| 463 |
-
};
|
| 464 |
-
|
| 465 |
-
const loadHistory = (item) => {
|
| 466 |
-
currentRequest.value.method = item.method;
|
| 467 |
-
currentRequest.value.url = item.url;
|
| 468 |
-
|
| 469 |
-
// Restore headers/params format
|
| 470 |
-
// Note: In history we stored headers as dict, need to convert back to array
|
| 471 |
-
if (item.headers && !Array.isArray(item.headers)) {
|
| 472 |
-
currentRequest.value.headers = Object.entries(item.headers).map(([k, v]) => ({ key: k, value: v, active: true }));
|
| 473 |
-
} else if (item.headers) {
|
| 474 |
-
currentRequest.value.headers = Object.entries(item.headers).map(([k, v]) => ({ key: k, value: v, active: true }));
|
| 475 |
-
}
|
| 476 |
-
|
| 477 |
-
if (item.params && !Array.isArray(item.params)) {
|
| 478 |
-
currentRequest.value.params = Object.entries(item.params).map(([k, v]) => ({ key: k, value: v, active: true }));
|
| 479 |
-
}
|
| 480 |
-
|
| 481 |
-
if (item.body) {
|
| 482 |
-
currentRequest.value.bodyContent = item.body;
|
| 483 |
-
// Auto detect type?
|
| 484 |
-
try {
|
| 485 |
-
JSON.parse(item.body);
|
| 486 |
-
currentRequest.value.bodyType = 'json';
|
| 487 |
-
} catch {
|
| 488 |
-
currentRequest.value.bodyType = 'text';
|
| 489 |
-
}
|
| 490 |
-
} else {
|
| 491 |
-
currentRequest.value.bodyType = 'none';
|
| 492 |
-
currentRequest.value.bodyContent = '';
|
| 493 |
-
}
|
| 494 |
-
};
|
| 495 |
-
|
| 496 |
const loadDemo = (type) => {
|
| 497 |
const demos = {
|
| 498 |
'get': {
|
|
@@ -518,7 +524,7 @@
|
|
| 518 |
method: 'GET',
|
| 519 |
url: 'https://httpbin.org/image/png',
|
| 520 |
params: [],
|
| 521 |
-
headers: [
|
| 522 |
bodyType: 'none',
|
| 523 |
bodyContent: ''
|
| 524 |
},
|
|
@@ -531,26 +537,186 @@
|
|
| 531 |
bodyContent: ''
|
| 532 |
}
|
| 533 |
};
|
| 534 |
-
|
| 535 |
const demo = demos[type];
|
| 536 |
if (demo) {
|
| 537 |
currentRequest.value = JSON.parse(JSON.stringify(demo));
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
}
|
| 544 |
};
|
| 545 |
|
| 546 |
const copyResponse = () => {
|
| 547 |
if (!response.value) return;
|
| 548 |
-
const content = response.value.is_json
|
|
|
|
|
|
|
|
|
|
| 549 |
navigator.clipboard.writeText(content).then(() => {
|
| 550 |
alert('已复制到剪贴板');
|
| 551 |
});
|
| 552 |
};
|
| 553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
return {
|
| 555 |
loading,
|
| 556 |
activeReqTab,
|
|
@@ -559,22 +725,26 @@
|
|
| 559 |
currentRequest,
|
| 560 |
paramsCount,
|
| 561 |
headersCount,
|
|
|
|
|
|
|
|
|
|
| 562 |
addParam,
|
| 563 |
removeParam,
|
| 564 |
addHeader,
|
| 565 |
removeHeader,
|
| 566 |
addAuthHeader,
|
| 567 |
-
formatBody,
|
| 568 |
-
sendRequest,
|
| 569 |
clearHistory,
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
copyResponse,
|
| 573 |
getMethodColor,
|
| 574 |
getStatusColor,
|
| 575 |
formatSize,
|
| 576 |
formatDate,
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
};
|
| 579 |
}
|
| 580 |
}).mount('#app');
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>HTTP 接口调试工作室 | HTTP Request Studio</title>
|
| 7 |
+
<!-- Local Static Assets -->
|
| 8 |
+
<script src="/static/js/vue.js"></script>
|
| 9 |
+
<script src="/static/js/tailwind.js"></script>
|
| 10 |
+
<link href="/static/css/prism.css" rel="stylesheet">
|
| 11 |
+
<script src="/static/js/prism.js"></script>
|
| 12 |
+
<script src="/static/js/prism-json.js"></script>
|
|
|
|
| 13 |
<style>
|
| 14 |
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
|
| 15 |
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
|
|
|
| 25 |
font-family: 'Fira Code', monospace;
|
| 26 |
tab-size: 2;
|
| 27 |
}
|
| 28 |
+
|
| 29 |
+
/* Glassmorphism utils */
|
| 30 |
+
.glass {
|
| 31 |
+
background: rgba(30, 41, 59, 0.7);
|
| 32 |
+
backdrop-filter: blur(10px);
|
| 33 |
+
-webkit-backdrop-filter: blur(10px);
|
| 34 |
+
}
|
| 35 |
</style>
|
| 36 |
</head>
|
| 37 |
+
<body class="bg-slate-900 text-slate-200 h-screen overflow-hidden flex flex-col">
|
| 38 |
|
| 39 |
<div id="app" class="w-full h-full flex flex-col">
|
| 40 |
<!-- Header -->
|
| 41 |
+
<header class="h-16 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 border-b border-slate-700 flex items-center px-6 justify-between shrink-0 shadow-lg z-10">
|
| 42 |
+
<div class="flex items-center space-x-4">
|
| 43 |
+
<div class="bg-gradient-to-br from-blue-500 to-indigo-600 w-10 h-10 rounded-lg shadow-lg flex items-center justify-center transform transition hover:scale-105">
|
| 44 |
+
<!-- Bolt Icon -->
|
| 45 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-white">
|
| 46 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
| 47 |
+
</svg>
|
| 48 |
+
</div>
|
| 49 |
+
<div>
|
| 50 |
+
<h1 class="font-bold text-xl text-white tracking-wide bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">HTTP Request Studio</h1>
|
| 51 |
+
<p class="text-[10px] text-slate-500 uppercase tracking-widest font-semibold">Pro Edition</p>
|
| 52 |
</div>
|
|
|
|
| 53 |
</div>
|
| 54 |
+
<div class="flex items-center space-x-4 text-sm">
|
| 55 |
+
<!-- Demo Dropdown -->
|
| 56 |
<div class="relative group">
|
| 57 |
+
<button class="text-slate-400 hover:text-white transition flex items-center bg-slate-800/50 hover:bg-slate-700 px-3 py-1.5 rounded-full border border-slate-700/50">
|
| 58 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5">
|
| 59 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 2.625v-8.25m0 0V3.75m0 3.75a6.01 6.01 0 01-1.5.189m1.5-.189a6.01 6.01 0 011.5.189m-6 2.25h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
|
| 60 |
+
</svg>
|
| 61 |
+
加载示例
|
| 62 |
</button>
|
| 63 |
+
<div class="absolute right-0 mt-2 w-48 bg-slate-800 border border-slate-700 rounded-lg shadow-xl hidden group-hover:block z-50 overflow-hidden">
|
| 64 |
+
<a href="#" @click.prevent="loadDemo('get')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">GET JSON Request</a>
|
| 65 |
+
<a href="#" @click.prevent="loadDemo('post')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">POST JSON Data</a>
|
| 66 |
+
<a href="#" @click.prevent="loadDemo('image')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">GET Image Binary</a>
|
| 67 |
+
<a href="#" @click.prevent="loadDemo('404')" class="block px-4 py-2 hover:bg-slate-700 text-slate-300 text-xs">404 Error Test</a>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
+
|
| 71 |
+
<div class="h-4 w-px bg-slate-700"></div>
|
| 72 |
+
|
| 73 |
+
<!-- Import cURL -->
|
| 74 |
+
<button @click="showImportModal = true" class="text-slate-400 hover:text-white transition flex items-center px-2">
|
| 75 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1.5">
|
| 76 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
| 77 |
+
</svg>
|
| 78 |
+
导入 cURL
|
| 79 |
+
</button>
|
| 80 |
+
|
| 81 |
+
<div class="h-4 w-px bg-slate-700"></div>
|
| 82 |
+
|
| 83 |
+
<span v-if="loading" class="text-blue-400 flex items-center animate-pulse">
|
| 84 |
+
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 85 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
| 86 |
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 87 |
+
</svg>
|
| 88 |
+
发送中...
|
| 89 |
</span>
|
| 90 |
+
|
| 91 |
+
<a href="https://huggingface.co/spaces/duqing26/http-request-studio" target="_blank" class="hover:text-white transition opacity-70 hover:opacity-100">
|
| 92 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
| 93 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
| 94 |
+
</svg>
|
| 95 |
+
</a>
|
| 96 |
</div>
|
| 97 |
</header>
|
| 98 |
|
| 99 |
<!-- Main Content -->
|
| 100 |
<div class="flex-1 flex overflow-hidden">
|
| 101 |
<!-- Sidebar (History) -->
|
| 102 |
+
<div class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0 transition-all duration-300" :class="{'w-0 opacity-0': !showSidebar}">
|
| 103 |
+
<div class="p-3 border-b border-slate-800 flex justify-between items-center bg-slate-800/20">
|
| 104 |
+
<span class="font-semibold text-slate-400 text-xs uppercase tracking-wider">History</span>
|
| 105 |
+
<button @click="clearHistory" class="text-xs text-slate-500 hover:text-red-400 p-1 rounded hover:bg-slate-800 transition" title="清空历史">
|
| 106 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
| 107 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
| 108 |
+
</svg>
|
| 109 |
</button>
|
| 110 |
</div>
|
| 111 |
+
<div class="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar">
|
| 112 |
+
<div v-if="history.length === 0" class="text-center text-slate-700 py-10 text-xs flex flex-col items-center">
|
| 113 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 mb-2 opacity-50">
|
| 114 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 115 |
+
</svg>
|
| 116 |
暂无记录
|
| 117 |
</div>
|
| 118 |
<div v-for="(item, index) in history" :key="index"
|
| 119 |
@click="loadHistory(item)"
|
| 120 |
+
class="group p-3 rounded-lg hover:bg-slate-800 cursor-pointer border border-transparent hover:border-slate-700 transition-all relative">
|
| 121 |
<div class="flex items-center justify-between mb-1">
|
| 122 |
+
<span :class="getMethodColor(item.method)" class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-opacity-10 w-auto">${ item.method }</span>
|
| 123 |
+
<span class="text-[10px] text-slate-500">${ formatDate(item.timestamp) }</span>
|
| 124 |
</div>
|
| 125 |
+
<div class="text-xs text-slate-300 truncate font-mono mb-1 opacity-80 group-hover:opacity-100" :title="item.url">${ item.url }</div>
|
| 126 |
+
<div class="flex items-center justify-between mt-1">
|
| 127 |
+
<div class="flex items-center space-x-2">
|
| 128 |
+
<span v-if="item.status" :class="getStatusColor(item.status)" class="text-[10px] px-1.5 py-0.5 rounded-full bg-opacity-20 font-medium">
|
| 129 |
+
${ item.status }
|
| 130 |
+
</span>
|
| 131 |
+
<span class="text-[10px] text-slate-600">${ item.duration }ms</span>
|
| 132 |
+
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
<!-- Workspace -->
|
| 139 |
+
<div class="flex-1 flex flex-col min-w-0 bg-slate-900 relative">
|
| 140 |
+
<!-- Toggle Sidebar Button -->
|
| 141 |
+
<button @click="showSidebar = !showSidebar" class="absolute top-1/2 left-0 -translate-y-1/2 z-10 bg-slate-800 border border-slate-700 text-slate-400 p-1 rounded-r shadow hover:text-white" style="width: 16px; height: 32px; display: flex; align-items: center; justify-content: center;">
|
| 142 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
|
| 143 |
+
<path v-if="showSidebar" stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
| 144 |
+
<path v-else stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
| 145 |
+
</svg>
|
| 146 |
+
</button>
|
| 147 |
+
|
| 148 |
<!-- Request Bar -->
|
| 149 |
+
<div class="p-4 border-b border-slate-800 bg-slate-800/30 backdrop-blur-sm z-10">
|
| 150 |
<div class="flex space-x-2">
|
| 151 |
+
<select v-model="currentRequest.method" class="bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-blue-500 font-bold w-28 transition shadow-sm hover:border-slate-500">
|
| 152 |
<option value="GET">GET</option>
|
| 153 |
<option value="POST">POST</option>
|
| 154 |
<option value="PUT">PUT</option>
|
|
|
|
| 157 |
<option value="HEAD">HEAD</option>
|
| 158 |
<option value="OPTIONS">OPTIONS</option>
|
| 159 |
</select>
|
| 160 |
+
<div class="flex-1 relative group">
|
| 161 |
<input type="text" v-model="currentRequest.url"
|
| 162 |
placeholder="输入 URL (例如: https://httpbin.org/get)"
|
| 163 |
@keyup.enter="sendRequest"
|
| 164 |
+
class="w-full bg-slate-800 border border-slate-600 text-white text-sm rounded-lg px-4 py-2.5 focus:outline-none focus:border-blue-500 font-mono transition shadow-sm hover:border-slate-500">
|
| 165 |
+
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-slate-600 group-focus-within:text-blue-500 transition">
|
| 166 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
| 167 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
| 168 |
+
</svg>
|
| 169 |
+
</div>
|
| 170 |
</div>
|
| 171 |
<button @click="sendRequest" :disabled="loading"
|
| 172 |
+
class="bg-blue-600 hover:bg-blue-500 text-white px-8 py-2.5 rounded-lg font-medium transition shadow-lg shadow-blue-900/50 flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none transform active:scale-95">
|
| 173 |
+
<svg v-if="!loading" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
| 174 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
| 175 |
+
</svg>
|
| 176 |
+
<svg v-else class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 177 |
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
| 178 |
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 179 |
+
</svg>
|
| 180 |
发送
|
| 181 |
</button>
|
| 182 |
</div>
|
|
|
|
| 186 |
<div class="flex-1 flex flex-col md:flex-row overflow-hidden">
|
| 187 |
|
| 188 |
<!-- Request Config (Left/Top) -->
|
| 189 |
+
<div class="flex-1 flex flex-col border-r border-slate-800 min-w-[300px]">
|
| 190 |
<!-- Tabs -->
|
| 191 |
+
<div class="flex border-b border-slate-800 bg-slate-900">
|
| 192 |
<button v-for="tab in ['Params', 'Headers', 'Body', 'Auth']"
|
| 193 |
@click="activeReqTab = tab"
|
| 194 |
+
:class="activeReqTab === tab ? 'text-blue-400 border-b-2 border-blue-400 bg-slate-800/50' : 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/30'"
|
| 195 |
+
class="px-6 py-3 text-sm font-medium transition flex items-center">
|
| 196 |
${ tab }
|
| 197 |
+
<span v-if="tab === 'Params' && paramsCount > 0" class="ml-2 text-[10px] bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded-full font-bold">${ paramsCount }</span>
|
| 198 |
+
<span v-if="tab === 'Headers' && headersCount > 0" class="ml-2 text-[10px] bg-blue-900 text-blue-300 px-1.5 py-0.5 rounded-full font-bold">${ headersCount }</span>
|
| 199 |
</button>
|
| 200 |
</div>
|
| 201 |
|
|
|
|
| 203 |
<div class="flex-1 overflow-y-auto p-4 bg-slate-900">
|
| 204 |
|
| 205 |
<!-- Params Tab -->
|
| 206 |
+
<div v-if="activeReqTab === 'Params'" class="animate-fade-in">
|
| 207 |
+
<div class="space-y-3">
|
| 208 |
+
<div class="flex justify-between items-center mb-2">
|
| 209 |
+
<div class="text-xs text-slate-500 uppercase tracking-wider font-semibold">Query Parameters</div>
|
| 210 |
+
<button @click="addParam" class="text-xs text-blue-400 hover:text-blue-300 flex items-center bg-slate-800 px-2 py-1 rounded hover:bg-slate-700 transition">
|
| 211 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
| 212 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
| 213 |
+
</svg>
|
| 214 |
+
添加参数
|
| 215 |
+
</button>
|
| 216 |
+
</div>
|
| 217 |
+
<div v-if="currentRequest.params.length === 0" class="text-center text-slate-600 py-8 text-sm italic">
|
| 218 |
+
没有查询参数
|
| 219 |
+
</div>
|
| 220 |
+
<div v-for="(param, idx) in currentRequest.params" :key="idx" class="flex space-x-2 group items-center">
|
| 221 |
+
<div class="pt-0">
|
| 222 |
+
<input type="checkbox" v-model="param.active" class="rounded bg-slate-700 border-slate-600 text-blue-500 focus:ring-offset-slate-900">
|
| 223 |
</div>
|
| 224 |
+
<input type="text" v-model="param.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
|
| 225 |
+
<input type="text" v-model="param.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
|
| 226 |
+
<button @click="removeParam(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2 transition">
|
| 227 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
| 228 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
| 229 |
+
</svg>
|
| 230 |
</button>
|
| 231 |
</div>
|
|
|
|
|
|
|
|
|
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
|
| 235 |
<!-- Headers Tab -->
|
| 236 |
+
<div v-if="activeReqTab === 'Headers'" class="animate-fade-in">
|
| 237 |
+
<div class="space-y-3">
|
| 238 |
+
<div class="flex justify-between items-center mb-2">
|
| 239 |
+
<div class="text-xs text-slate-500 uppercase tracking-wider font-semibold">Request Headers</div>
|
| 240 |
+
<button @click="addHeader" class="text-xs text-blue-400 hover:text-blue-300 flex items-center bg-slate-800 px-2 py-1 rounded hover:bg-slate-700 transition">
|
| 241 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
| 242 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
| 243 |
+
</svg>
|
| 244 |
+
添加 Header
|
| 245 |
+
</button>
|
| 246 |
+
</div>
|
| 247 |
+
<div v-for="(header, idx) in currentRequest.headers" :key="idx" class="flex space-x-2 group items-center">
|
| 248 |
+
<div class="pt-0">
|
| 249 |
+
<input type="checkbox" v-model="header.active" class="rounded bg-slate-700 border-slate-600 text-blue-500 focus:ring-offset-slate-900">
|
| 250 |
</div>
|
| 251 |
+
<input type="text" v-model="header.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600 font-mono text-xs">
|
| 252 |
+
<input type="text" v-model="header.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-300 focus:border-blue-500 outline-none transition placeholder-slate-600">
|
| 253 |
+
<button @click="removeHeader(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2 transition">
|
| 254 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
| 255 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
| 256 |
+
</svg>
|
| 257 |
</button>
|
| 258 |
</div>
|
|
|
|
|
|
|
|
|
|
| 259 |
</div>
|
| 260 |
</div>
|
| 261 |
|
| 262 |
<!-- Body Tab -->
|
| 263 |
+
<div v-if="activeReqTab === 'Body'" class="animate-fade-in h-full flex flex-col">
|
| 264 |
+
<div class="flex items-center space-x-6 mb-4 px-1">
|
| 265 |
+
<label class="flex items-center space-x-2 cursor-pointer group">
|
| 266 |
+
<input type="radio" v-model="currentRequest.bodyType" value="none" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
|
| 267 |
+
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">None</span>
|
| 268 |
</label>
|
| 269 |
+
<label class="flex items-center space-x-2 cursor-pointer group">
|
| 270 |
+
<input type="radio" v-model="currentRequest.bodyType" value="json" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
|
| 271 |
+
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">JSON</span>
|
| 272 |
</label>
|
| 273 |
+
<label class="flex items-center space-x-2 cursor-pointer group">
|
| 274 |
+
<input type="radio" v-model="currentRequest.bodyType" value="text" class="text-blue-500 focus:ring-blue-500 bg-slate-700 border-slate-600 focus:ring-offset-slate-900">
|
| 275 |
+
<span class="text-sm text-slate-400 group-hover:text-slate-200 transition">Text</span>
|
| 276 |
</label>
|
| 277 |
</div>
|
| 278 |
+
<div v-if="currentRequest.bodyType === 'json'" class="flex-1 relative border border-slate-700 rounded-lg overflow-hidden">
|
| 279 |
+
<button @click="formatBody" class="absolute top-2 right-2 z-10 text-xs bg-slate-800 hover:bg-slate-700 text-slate-300 px-2 py-1 rounded border border-slate-600 transition opacity-70 hover:opacity-100">
|
| 280 |
+
Format JSON
|
| 281 |
</button>
|
| 282 |
<textarea v-model="currentRequest.bodyContent"
|
| 283 |
+
class="w-full h-full bg-slate-800 p-4 font-mono text-sm text-green-400 focus:border-blue-500 outline-none resize-none code-editor"
|
| 284 |
placeholder="{ 'key': 'value' }"></textarea>
|
| 285 |
</div>
|
| 286 |
+
<div v-if="currentRequest.bodyType === 'text'" class="flex-1 border border-slate-700 rounded-lg overflow-hidden">
|
| 287 |
<textarea v-model="currentRequest.bodyContent"
|
| 288 |
+
class="w-full h-full bg-slate-800 p-4 font-mono text-sm text-slate-300 focus:border-blue-500 outline-none resize-none"
|
| 289 |
placeholder="Raw text content..."></textarea>
|
| 290 |
</div>
|
| 291 |
+
<div v-if="currentRequest.bodyType === 'none'" class="flex-1 flex flex-col items-center justify-center text-slate-600 italic border border-dashed border-slate-800 rounded-lg">
|
| 292 |
+
<span class="mb-2">This request has no body</span>
|
| 293 |
+
<span class="text-xs opacity-50">Select JSON or Text to add content</span>
|
| 294 |
</div>
|
| 295 |
</div>
|
| 296 |
|
| 297 |
<!-- Auth Tab (Simple) -->
|
| 298 |
+
<div v-if="activeReqTab === 'Auth'" class="animate-fade-in">
|
| 299 |
+
<div class="bg-slate-800/50 p-6 rounded-lg border border-slate-700 text-center">
|
| 300 |
+
<div class="text-sm text-slate-400 mb-4">目前仅支持通过 Headers 添加认证信息。</div>
|
| 301 |
+
<button @click="addAuthHeader" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded text-sm transition shadow-lg shadow-blue-900/30">
|
| 302 |
+
添加 Bearer Token Header
|
| 303 |
+
</button>
|
| 304 |
+
<p class="text-xs text-slate-500 mt-4">更多认证方式(Basic Auth, OAuth2)正在开发中...</p>
|
| 305 |
+
</div>
|
| 306 |
</div>
|
| 307 |
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
|
| 311 |
<!-- Response (Right/Bottom) -->
|
| 312 |
+
<div class="flex-1 flex flex-col min-w-[300px] bg-slate-900 border-l border-slate-800">
|
| 313 |
<div v-if="response" class="h-full flex flex-col">
|
| 314 |
<!-- Response Meta -->
|
| 315 |
+
<div class="px-4 py-3 border-b border-slate-800 bg-slate-800/20 flex justify-between items-center">
|
| 316 |
+
<div class="flex items-center space-x-6">
|
| 317 |
+
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Status</span>
|
| 318 |
+
<span :class="getStatusColor(response.status)" class="font-bold text-sm px-2 py-0.5 rounded-full">${ response.status } ${ response.status_text }</span>
|
| 319 |
+
|
| 320 |
+
<div class="h-4 w-px bg-slate-700"></div>
|
| 321 |
+
|
| 322 |
+
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Time</span>
|
| 323 |
+
<span class="text-sm text-slate-200 font-mono">${ response.duration }ms</span>
|
| 324 |
+
|
| 325 |
+
<div class="h-4 w-px bg-slate-700"></div>
|
| 326 |
+
|
| 327 |
+
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Size</span>
|
| 328 |
+
<span class="text-sm text-slate-200 font-mono">${ formatSize(response.size) }</span>
|
| 329 |
</div>
|
| 330 |
+
<div class="flex space-x-2">
|
| 331 |
+
<button @click="copyResponse" class="text-slate-400 hover:text-white text-xs bg-slate-800 px-2 py-1 rounded border border-slate-700 transition flex items-center" title="复制结果">
|
| 332 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
| 333 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5" />
|
| 334 |
+
</svg>
|
| 335 |
+
Copy
|
| 336 |
</button>
|
| 337 |
</div>
|
| 338 |
</div>
|
|
|
|
| 341 |
<div class="flex-1 overflow-hidden relative group bg-slate-900">
|
| 342 |
<div v-if="response.is_binary" class="w-full h-full flex flex-col items-center justify-center p-4 overflow-auto">
|
| 343 |
<div v-if="response.content_type && response.content_type.startsWith('image/')" class="text-center">
|
| 344 |
+
<img :src="`data:${response.content_type};base64,${response.data}`" class="max-w-full max-h-[70vh] shadow-2xl rounded-lg border border-slate-700 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iIzMzMyIgZmlsbC1vcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0wIDBoMTB2MTBIMHptMTAgMTBoMTB2MTBIMTB6Ii8+PC9zdmc+')] bg-repeat" />
|
| 345 |
+
<div class="mt-4 px-3 py-1 bg-slate-800 rounded-full inline-block text-xs text-slate-400 border border-slate-700 font-mono">${ response.content_type }</div>
|
| 346 |
</div>
|
| 347 |
<div v-else class="text-center">
|
| 348 |
+
<div class="w-20 h-20 bg-slate-800 rounded-full flex items-center justify-center mx-auto mb-4 border border-slate-700">
|
| 349 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10 text-slate-500">
|
| 350 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
| 351 |
+
</svg>
|
| 352 |
+
</div>
|
| 353 |
+
<p class="text-slate-300 font-medium text-lg">Binary Content</p>
|
| 354 |
+
<p class="text-xs text-slate-500 mt-1 font-mono">${ response.content_type }</p>
|
| 355 |
+
<p class="text-xs text-slate-600 mt-4 bg-slate-800/50 px-4 py-2 rounded">Preview unavailable for this file type</p>
|
| 356 |
</div>
|
| 357 |
</div>
|
| 358 |
+
<div v-else class="w-full h-full relative">
|
| 359 |
+
<pre v-if="response.is_json" class="w-full h-full p-4 overflow-auto text-sm font-mono language-json custom-scrollbar"><code class="language-json">${ formatResponseData(response.data) }</code></pre>
|
| 360 |
+
<pre v-else class="w-full h-full p-4 overflow-auto text-sm font-mono text-slate-300 whitespace-pre-wrap custom-scrollbar">${ response.data }</pre>
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
</div>
|
| 364 |
|
| 365 |
<div v-else class="flex-1 flex flex-col items-center justify-center text-slate-600">
|
| 366 |
+
<div class="w-24 h-24 bg-slate-800/50 rounded-full flex items-center justify-center mb-6">
|
| 367 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 opacity-30">
|
| 368 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
| 369 |
+
</svg>
|
| 370 |
+
</div>
|
| 371 |
+
<p class="text-lg font-medium text-slate-500">Ready to Request</p>
|
| 372 |
+
<p class="text-sm text-slate-600 mt-2">Enter URL and click Send to see the response</p>
|
| 373 |
</div>
|
| 374 |
</div>
|
| 375 |
|
|
|
|
| 378 |
</div>
|
| 379 |
</div>
|
| 380 |
|
| 381 |
+
<!-- Import Modal -->
|
| 382 |
+
<div v-if="showImportModal" class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50">
|
| 383 |
+
<div class="bg-slate-800 border border-slate-700 rounded-lg shadow-2xl w-[600px] max-w-full m-4 p-6 animate-scale-in">
|
| 384 |
+
<h3 class="text-lg font-bold text-white mb-4">Import cURL</h3>
|
| 385 |
+
<textarea v-model="curlInput" class="w-full h-40 bg-slate-900 border border-slate-700 rounded p-3 font-mono text-sm text-slate-300 focus:border-blue-500 outline-none resize-none mb-4" placeholder="Paste cURL command here..."></textarea>
|
| 386 |
+
<div class="flex justify-end space-x-3">
|
| 387 |
+
<button @click="showImportModal = false" class="px-4 py-2 rounded text-slate-400 hover:text-white hover:bg-slate-700 transition text-sm">Cancel</button>
|
| 388 |
+
<button @click="importCurl" class="px-4 py-2 rounded bg-blue-600 hover:bg-blue-500 text-white font-medium transition text-sm">Import</button>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
<script>
|
| 394 |
const { createApp, ref, computed, onMounted, nextTick } = Vue;
|
| 395 |
|
|
|
|
| 400 |
const activeReqTab = ref('Params');
|
| 401 |
const history = ref([]);
|
| 402 |
const response = ref(null);
|
| 403 |
+
const showSidebar = ref(true);
|
| 404 |
+
const showImportModal = ref(false);
|
| 405 |
+
const curlInput = ref('');
|
| 406 |
|
| 407 |
const currentRequest = ref({
|
| 408 |
method: 'GET',
|
|
|
|
| 423 |
history.value = JSON.parse(saved);
|
| 424 |
}
|
| 425 |
|
| 426 |
+
// Add default header if empty
|
| 427 |
if (currentRequest.value.headers.length === 0 || (currentRequest.value.headers.length === 1 && !currentRequest.value.headers[0].key)) {
|
| 428 |
currentRequest.value.headers = [
|
| 429 |
{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true },
|
|
|
|
| 446 |
activeReqTab.value = 'Headers';
|
| 447 |
};
|
| 448 |
|
| 449 |
+
const clearHistory = () => {
|
| 450 |
+
if(confirm('确定清空所有历史记录吗?')) {
|
| 451 |
+
history.value = [];
|
| 452 |
+
localStorage.removeItem('http-studio-history');
|
| 453 |
+
}
|
| 454 |
+
};
|
| 455 |
+
|
| 456 |
const formatBody = () => {
|
| 457 |
try {
|
| 458 |
const obj = JSON.parse(currentRequest.value.bodyContent);
|
|
|
|
| 481 |
};
|
| 482 |
|
| 483 |
const getStatusColor = (status) => {
|
| 484 |
+
if (status >= 200 && status < 300) return 'text-green-400 bg-green-900/50';
|
| 485 |
+
if (status >= 300 && status < 400) return 'text-yellow-400 bg-yellow-900/50';
|
| 486 |
+
if (status >= 400 && status < 500) return 'text-orange-400 bg-orange-900/50';
|
| 487 |
+
if (status >= 500) return 'text-red-400 bg-red-900/50';
|
| 488 |
+
return 'text-slate-400 bg-slate-800';
|
| 489 |
};
|
| 490 |
|
| 491 |
const formatSize = (bytes) => {
|
|
|
|
| 499 |
return new Date(ts).toLocaleTimeString();
|
| 500 |
};
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
const loadDemo = (type) => {
|
| 503 |
const demos = {
|
| 504 |
'get': {
|
|
|
|
| 524 |
method: 'GET',
|
| 525 |
url: 'https://httpbin.org/image/png',
|
| 526 |
params: [],
|
| 527 |
+
headers: [],
|
| 528 |
bodyType: 'none',
|
| 529 |
bodyContent: ''
|
| 530 |
},
|
|
|
|
| 537 |
bodyContent: ''
|
| 538 |
}
|
| 539 |
};
|
| 540 |
+
|
| 541 |
const demo = demos[type];
|
| 542 |
if (demo) {
|
| 543 |
currentRequest.value = JSON.parse(JSON.stringify(demo));
|
| 544 |
+
// Add default user agent
|
| 545 |
+
currentRequest.value.headers.push({ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true });
|
| 546 |
+
}
|
| 547 |
+
};
|
| 548 |
+
|
| 549 |
+
const loadHistory = (item) => {
|
| 550 |
+
currentRequest.value.method = item.method;
|
| 551 |
+
currentRequest.value.url = item.url;
|
| 552 |
+
|
| 553 |
+
if (item.headers) {
|
| 554 |
+
currentRequest.value.headers = Array.isArray(item.headers) ? item.headers :
|
| 555 |
+
Object.entries(item.headers).map(([k, v]) => ({ key: k, value: v, active: true }));
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
if (item.params) {
|
| 559 |
+
currentRequest.value.params = Array.isArray(item.params) ? item.params :
|
| 560 |
+
Object.entries(item.params).map(([k, v]) => ({ key: k, value: v, active: true }));
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
if (item.body) {
|
| 564 |
+
currentRequest.value.bodyContent = item.body;
|
| 565 |
+
try {
|
| 566 |
+
JSON.parse(item.body);
|
| 567 |
+
currentRequest.value.bodyType = 'json';
|
| 568 |
+
} catch {
|
| 569 |
+
currentRequest.value.bodyType = 'text';
|
| 570 |
+
}
|
| 571 |
+
} else {
|
| 572 |
+
currentRequest.value.bodyType = 'none';
|
| 573 |
+
currentRequest.value.bodyContent = '';
|
| 574 |
}
|
| 575 |
};
|
| 576 |
|
| 577 |
const copyResponse = () => {
|
| 578 |
if (!response.value) return;
|
| 579 |
+
const content = response.value.is_json
|
| 580 |
+
? JSON.stringify(response.value.data, null, 2)
|
| 581 |
+
: response.value.data;
|
| 582 |
+
|
| 583 |
navigator.clipboard.writeText(content).then(() => {
|
| 584 |
alert('已复制到剪贴板');
|
| 585 |
});
|
| 586 |
};
|
| 587 |
|
| 588 |
+
const importCurl = () => {
|
| 589 |
+
const curl = curlInput.value.trim();
|
| 590 |
+
if (!curl) return;
|
| 591 |
+
|
| 592 |
+
// Basic cURL parser
|
| 593 |
+
// Note: This is a simplified parser
|
| 594 |
+
try {
|
| 595 |
+
const methodMatch = curl.match(/-X\s+([A-Z]+)/) || curl.match(/--request\s+([A-Z]+)/);
|
| 596 |
+
const method = methodMatch ? methodMatch[1] : 'GET';
|
| 597 |
+
|
| 598 |
+
// Extract URL (naively)
|
| 599 |
+
const urlMatch = curl.match(/['"](http[s]?:\/\/[^'"]+)['"]/) || curl.match(/(http[s]?:\/\/[^\s]+)/);
|
| 600 |
+
const url = urlMatch ? urlMatch[1] : '';
|
| 601 |
+
|
| 602 |
+
if (!url) {
|
| 603 |
+
alert('无法解析 URL');
|
| 604 |
+
return;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
// Extract Headers
|
| 608 |
+
const headers = [];
|
| 609 |
+
const headerRegex = /-H\s+['"]([^'"]+)['"]/g;
|
| 610 |
+
let match;
|
| 611 |
+
while ((match = headerRegex.exec(curl)) !== null) {
|
| 612 |
+
const [key, value] = match[1].split(/:\s*(.*)/);
|
| 613 |
+
headers.push({ key: key.trim(), value: value.trim(), active: true });
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
// Extract Data
|
| 617 |
+
const dataMatch = curl.match(/-d\s+['"]([^'"]+)['"]/) || curl.match(/--data\s+['"]([^'"]+)['"]/) || curl.match(/--data-raw\s+['"]([^'"]+)['"]/);
|
| 618 |
+
const body = dataMatch ? dataMatch[1] : '';
|
| 619 |
+
|
| 620 |
+
currentRequest.value = {
|
| 621 |
+
method,
|
| 622 |
+
url,
|
| 623 |
+
headers: headers.length ? headers : [{ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true }],
|
| 624 |
+
params: [],
|
| 625 |
+
bodyType: body ? (isValidJson(body) ? 'json' : 'text') : 'none',
|
| 626 |
+
bodyContent: body
|
| 627 |
+
};
|
| 628 |
+
|
| 629 |
+
showImportModal.value = false;
|
| 630 |
+
curlInput.value = '';
|
| 631 |
+
} catch (e) {
|
| 632 |
+
alert('解析 cURL 失败: ' + e.message);
|
| 633 |
+
}
|
| 634 |
+
};
|
| 635 |
+
|
| 636 |
+
const isValidJson = (str) => {
|
| 637 |
+
try { JSON.parse(str); return true; } catch { return false; }
|
| 638 |
+
};
|
| 639 |
+
|
| 640 |
+
const sendRequest = async () => {
|
| 641 |
+
if (!currentRequest.value.url) {
|
| 642 |
+
alert('请输入 URL');
|
| 643 |
+
return;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
loading.value = true;
|
| 647 |
+
response.value = null;
|
| 648 |
+
|
| 649 |
+
// Prepare payload
|
| 650 |
+
const headers = {};
|
| 651 |
+
currentRequest.value.headers.forEach(h => {
|
| 652 |
+
if (h.active && h.key) headers[h.key] = h.value;
|
| 653 |
+
});
|
| 654 |
+
|
| 655 |
+
const params = {};
|
| 656 |
+
currentRequest.value.params.forEach(p => {
|
| 657 |
+
if (p.active && p.key) params[p.key] = p.value;
|
| 658 |
+
});
|
| 659 |
+
|
| 660 |
+
// Set Content-Type if JSON body
|
| 661 |
+
if (currentRequest.value.bodyType === 'json') {
|
| 662 |
+
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
const payload = {
|
| 666 |
+
method: currentRequest.value.method,
|
| 667 |
+
url: currentRequest.value.url,
|
| 668 |
+
params: params,
|
| 669 |
+
headers: headers,
|
| 670 |
+
body: currentRequest.value.bodyType !== 'none' ? currentRequest.value.bodyContent : null
|
| 671 |
+
};
|
| 672 |
+
|
| 673 |
+
try {
|
| 674 |
+
const res = await fetch('/api/request', {
|
| 675 |
+
method: 'POST',
|
| 676 |
+
headers: {
|
| 677 |
+
'Content-Type': 'application/json'
|
| 678 |
+
},
|
| 679 |
+
body: JSON.stringify(payload)
|
| 680 |
+
});
|
| 681 |
+
|
| 682 |
+
const data = await res.json();
|
| 683 |
+
if (data.error) {
|
| 684 |
+
alert('请求失败: ' + data.error);
|
| 685 |
+
response.value = {
|
| 686 |
+
status: 0,
|
| 687 |
+
status_text: 'Error',
|
| 688 |
+
duration: 0,
|
| 689 |
+
size: 0,
|
| 690 |
+
data: data.error,
|
| 691 |
+
is_json: false
|
| 692 |
+
};
|
| 693 |
+
} else {
|
| 694 |
+
response.value = data;
|
| 695 |
+
|
| 696 |
+
// Save to history
|
| 697 |
+
const historyItem = {
|
| 698 |
+
...currentRequest.value,
|
| 699 |
+
timestamp: Date.now(),
|
| 700 |
+
status: data.status,
|
| 701 |
+
duration: data.duration
|
| 702 |
+
};
|
| 703 |
+
|
| 704 |
+
// Remove deep clone of headers/params for storage to save space, but here we keep structure
|
| 705 |
+
// Limit history size
|
| 706 |
+
history.value.unshift(historyItem);
|
| 707 |
+
if (history.value.length > 50) history.value.pop();
|
| 708 |
+
localStorage.setItem('http-studio-history', JSON.stringify(history.value));
|
| 709 |
+
}
|
| 710 |
+
} catch (e) {
|
| 711 |
+
alert('网络错误: ' + e.message);
|
| 712 |
+
} finally {
|
| 713 |
+
loading.value = false;
|
| 714 |
+
nextTick(() => {
|
| 715 |
+
if(window.Prism) Prism.highlightAll();
|
| 716 |
+
});
|
| 717 |
+
}
|
| 718 |
+
};
|
| 719 |
+
|
| 720 |
return {
|
| 721 |
loading,
|
| 722 |
activeReqTab,
|
|
|
|
| 725 |
currentRequest,
|
| 726 |
paramsCount,
|
| 727 |
headersCount,
|
| 728 |
+
showSidebar,
|
| 729 |
+
showImportModal,
|
| 730 |
+
curlInput,
|
| 731 |
addParam,
|
| 732 |
removeParam,
|
| 733 |
addHeader,
|
| 734 |
removeHeader,
|
| 735 |
addAuthHeader,
|
|
|
|
|
|
|
| 736 |
clearHistory,
|
| 737 |
+
formatBody,
|
| 738 |
+
formatResponseData,
|
|
|
|
| 739 |
getMethodColor,
|
| 740 |
getStatusColor,
|
| 741 |
formatSize,
|
| 742 |
formatDate,
|
| 743 |
+
sendRequest,
|
| 744 |
+
loadDemo,
|
| 745 |
+
loadHistory,
|
| 746 |
+
copyResponse,
|
| 747 |
+
importCurl
|
| 748 |
};
|
| 749 |
}
|
| 750 |
}).mount('#app');
|