Trae Assistant commited on
Commit
03b3117
·
1 Parent(s): 2d8c38a
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,"&amp;").replace(/</g,"&lt;").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,"&quot;")+'"';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(/&amp;/,"&"))}),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
- <script src="https://lib.baomitu.com/vue/3.3.4/vue.global.prod.min.js"></script>
8
- <script src="https://lib.baomitu.com/tailwindcss/3.3.3/tailwind.min.js"></script>
9
- <link href="https://lib.baomitu.com/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
10
- <!-- Prism for syntax highlighting -->
11
- <link href="https://lib.baomitu.com/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
12
- <script src="https://lib.baomitu.com/prism/1.29.0/prism.min.js"></script>
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-14 bg-slate-800 border-b border-slate-700 flex items-center px-4 justify-between shrink-0">
36
- <div class="flex items-center space-x-3">
37
- <div class="bg-blue-600 w-8 h-8 rounded flex items-center justify-center">
38
- <i class="fa-solid fa-bolt text-white"></i>
 
 
 
 
 
 
 
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 text-slate-400">
 
43
  <div class="relative group">
44
- <button class="hover:text-white transition flex items-center">
45
- <i class="fa-solid fa-lightbulb mr-1"></i> 示例
 
 
 
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
- <span v-if="loading" class="text-blue-400 flex items-center">
55
- <i class="fa-solid fa-spinner fa-spin mr-2"></i> 请求中...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </span>
57
- <a href="https://github.com/trae-ai" target="_blank" class="hover:text-white transition"><i class="fa-brands fa-github"></i></a>
 
 
 
 
 
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-800 border-r border-slate-700 flex flex-col shrink-0">
65
- <div class="p-3 border-b border-slate-700 flex justify-between items-center">
66
- <span class="font-semibold text-slate-300">历史记录</span>
67
- <button @click="clearHistory" class="text-xs text-slate-500 hover:text-red-400" title="清空历史">
68
- <i class="fa-solid fa-trash"></i>
 
 
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-600 py-10 text-sm">
 
 
 
73
  暂无记录
74
  </div>
75
  <div v-for="(item, index) in history" :key="index"
76
  @click="loadHistory(item)"
77
- class="group p-2 rounded hover:bg-slate-700 cursor-pointer border border-transparent hover:border-slate-600 transition-all">
78
  <div class="flex items-center justify-between mb-1">
79
- <span :class="getMethodColor(item.method)" class="text-xs font-bold w-12">${ item.method }</span>
80
- <span class="text-xs text-slate-500">${ formatDate(item.timestamp) }</span>
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 mt-1 space-x-2">
84
- <span v-if="item.status" :class="getStatusColor(item.status)" class="text-[10px] px-1 rounded bg-opacity-20">
85
- ${ item.status }
86
- </span>
87
- <span class="text-[10px] text-slate-500">${ item.duration }ms</span>
 
 
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-700 bg-slate-800/50">
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-3 py-2 focus:outline-none focus:border-blue-500 font-mono">
 
 
 
 
 
113
  </div>
114
  <button @click="sendRequest" :disabled="loading"
115
- class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded font-medium transition flex items-center disabled:opacity-50 disabled:cursor-not-allowed">
116
- <i v-if="!loading" class="fa-solid fa-paper-plane mr-2"></i>
117
- <i v-else" class="fa-solid fa-spinner fa-spin mr-2"></i>
 
 
 
 
 
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-700 min-w-[300px]">
128
  <!-- Tabs -->
129
- <div class="flex border-b border-slate-700 bg-slate-800/30">
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-4 py-2 text-sm font-medium transition">
134
  ${ tab }
135
- <span v-if="tab === 'Params' && paramsCount > 0" class="ml-1 text-[10px] bg-slate-700 px-1 rounded-full text-slate-300">${ paramsCount }</span>
136
- <span v-if="tab === 'Headers' && headersCount > 0" class="ml-1 text-[10px] bg-slate-700 px-1 rounded-full text-slate-300">${ headersCount }</span>
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-2">
146
- <div class="text-xs text-slate-500 mb-2">查询参数 (Query Parameters)</div>
147
- <div v-for="(param, idx) in currentRequest.params" :key="idx" class="flex space-x-2 group">
148
- <div class="pt-2">
149
- <input type="checkbox" v-model="param.active" class="rounded bg-slate-700 border-slate-600">
 
 
 
 
 
 
 
 
 
 
 
150
  </div>
151
- <input type="text" v-model="param.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-300 focus:border-blue-500 outline-none">
152
- <input type="text" v-model="param.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-300 focus:border-blue-500 outline-none">
153
- <button @click="removeParam(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2">
154
- <i class="fa-solid fa-times"></i>
 
 
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-2">
166
- <div class="text-xs text-slate-500 mb-2">请求头 (Headers)</div>
167
- <div v-for="(header, idx) in currentRequest.headers" :key="idx" class="flex space-x-2 group">
168
- <div class="pt-2">
169
- <input type="checkbox" v-model="header.active" class="rounded bg-slate-700 border-slate-600">
 
 
 
 
 
 
 
 
170
  </div>
171
- <input type="text" v-model="header.key" placeholder="Key" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-300 focus:border-blue-500 outline-none list-header-keys">
172
- <input type="text" v-model="header.value" placeholder="Value" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm text-slate-300 focus:border-blue-500 outline-none">
173
- <button @click="removeHeader(idx)" class="text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 px-2">
174
- <i class="fa-solid fa-times"></i>
 
 
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-4 mb-3">
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-300">None</span>
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-300">JSON</span>
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-300">Text</span>
197
  </label>
198
  </div>
199
- <div v-if="currentRequest.bodyType === 'json'" class="h-[300px] relative">
200
- <button @click="formatBody" class="absolute top-2 right-2 z-10 text-xs bg-slate-700 hover:bg-slate-600 text-slate-300 px-2 py-1 rounded border border-slate-600">
201
- Beautify
202
  </button>
203
  <textarea v-model="currentRequest.bodyContent"
204
- class="w-full h-full bg-slate-800 border border-slate-700 rounded p-3 font-mono text-sm text-green-400 focus:border-blue-500 outline-none resize-none code-editor"
205
  placeholder="{ 'key': 'value' }"></textarea>
206
  </div>
207
- <div v-if="currentRequest.bodyType === 'text'" class="h-[300px]">
208
  <textarea v-model="currentRequest.bodyContent"
209
- class="w-full h-full bg-slate-800 border border-slate-700 rounded p-3 font-mono text-sm text-slate-300 focus:border-blue-500 outline-none resize-none"
210
  placeholder="Raw text content..."></textarea>
211
  </div>
212
- <div v-if="currentRequest.bodyType === 'none'" class="h-[300px] flex items-center justify-center text-slate-600 italic">
213
- 该请求没有 Body 内容
 
214
  </div>
215
  </div>
216
 
217
  <!-- Auth Tab (Simple) -->
218
- <div v-if="activeReqTab === 'Auth'">
219
- <div class="text-sm text-slate-400 mb-4">目前仅支持通过 Headers 添加认证信息。请切换到 Headers 标签页添加 `Authorization` 头。</div>
220
- <button @click="addAuthHeader" class="bg-slate-700 hover:bg-slate-600 text-white px-3 py-1 rounded text-sm">
221
- 添加 Bearer Token Header
222
- </button>
 
 
 
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-700">
230
  <div v-if="response" class="h-full flex flex-col">
231
  <!-- Response Meta -->
232
- <div class="px-4 py-2 border-b border-slate-700 bg-slate-800/30 flex justify-between items-center">
233
- <div class="flex items-center space-x-4">
234
- <span class="text-sm font-medium">Status:
235
- <span :class="getStatusColor(response.status)" class="font-bold ml-1">${ response.status } ${ response.status_text }</span>
236
- </span>
237
- <span class="text-sm text-slate-400">Time: <span class="text-slate-200">${ response.duration }ms</span></span>
238
- <span class="text-sm text-slate-400">Size: <span class="text-slate-200">${ formatSize(response.size) }</span></span>
 
 
 
 
 
 
 
239
  </div>
240
- <div>
241
- <button @click="copyResponse" class="text-slate-400 hover:text-white text-sm" title="复制结果">
242
- <i class="fa-regular fa-copy"></i>
 
 
 
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-[80vh] shadow-lg rounded border border-slate-700 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iIzMzMyIgZmlsbC1vcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0wIDBoMTB2MTBIMHptMTAgMTBoMTB2MTBIMTB6Ii8+PC9zdmc+')] bg-repeat" />
252
- <div class="mt-2 text-xs text-slate-500">${ response.content_type }</div>
253
  </div>
254
  <div v-else class="text-center">
255
- <i class="fa-solid fa-file-binary text-5xl mb-4 text-slate-600"></i>
256
- <p class="text-slate-400 font-medium">Binary Content</p>
257
- <p class="text-xs text-slate-600 mt-1 font-mono">${ response.content_type }</p>
258
- <p class="text-xs text-slate-600 mt-2">预览不可用 (Preview unavailable)</p>
 
 
 
 
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
- <i class="fa-solid fa-paper-plane text-4xl mb-4 opacity-20"></i>
270
- <p>输入 URL 并点击发送以查看响应</p>
 
 
 
 
 
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: [{ key: 'Accept', value: 'image/png', active: true }],
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
- activeReqTab.value = demo.bodyType !== 'none' ? 'Body' : 'Params';
539
- // Add default user agent if missing
540
- if (!currentRequest.value.headers.find(h => h.key === 'User-Agent')) {
541
- currentRequest.value.headers.push({ key: 'User-Agent', value: 'HttpRequestStudio/1.0', active: true });
542
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  }
544
  };
545
 
546
  const copyResponse = () => {
547
  if (!response.value) return;
548
- const content = response.value.is_json ? JSON.stringify(response.value.data, null, 2) : response.value.data;
 
 
 
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
- loadHistory,
571
- loadDemo,
572
- copyResponse,
573
  getMethodColor,
574
  getStatusColor,
575
  formatSize,
576
  formatDate,
577
- formatResponseData
 
 
 
 
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');