138 lines
4.9 KiB
Vue
138 lines
4.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
|
import { useRequestStore } from '../stores/requestStore';
|
|
import { Clipboard, Check } from 'lucide-vue-next';
|
|
|
|
const CodeEditor = defineAsyncComponent(() => import('./CodeEditor.vue'));
|
|
const store = useRequestStore();
|
|
const isCopied = ref(false);
|
|
const activeTab = ref<'body' | 'headers'>('body');
|
|
|
|
const isJsonResponse = computed(() => {
|
|
const response = store.activeRequest.response;
|
|
if (!response) return false;
|
|
|
|
const contentType = response.headers['content-type'] || response.headers['Content-Type'] || '';
|
|
if (contentType.toLowerCase().includes('json')) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
JSON.parse(response.body);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
const formattedHeaders = computed(() => {
|
|
const response = store.activeRequest.response;
|
|
if (!response) return '';
|
|
|
|
return Object.entries(response.headers)
|
|
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
.join('\n');
|
|
});
|
|
|
|
const formattedBody = computed(() => {
|
|
if (!store.activeRequest.response) return '';
|
|
if (!isJsonResponse.value) return store.activeRequest.response.body;
|
|
try {
|
|
return JSON.stringify(JSON.parse(store.activeRequest.response.body), null, 2);
|
|
} catch {
|
|
return store.activeRequest.response.body;
|
|
}
|
|
});
|
|
|
|
const displayedContent = computed(() => (
|
|
activeTab.value === 'headers' ? formattedHeaders.value : formattedBody.value
|
|
));
|
|
|
|
const statusColor = computed(() => {
|
|
const s = store.activeRequest.response?.status || 0;
|
|
if (s >= 200 && s < 300) return 'text-emerald-400';
|
|
if (s >= 400) return 'text-rose-400';
|
|
return 'text-amber-400';
|
|
});
|
|
|
|
const copyToClipboard = async () => {
|
|
if (!displayedContent.value) return;
|
|
try {
|
|
await navigator.clipboard.writeText(displayedContent.value);
|
|
isCopied.value = true;
|
|
setTimeout(() => {
|
|
isCopied.value = false;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy:', err);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col h-full border-t border-slate-800 bg-slate-900">
|
|
<div v-if="store.activeRequest.response" class="flex flex-col h-full">
|
|
<!-- Meta Bar -->
|
|
<div class="px-4 py-2 border-b border-slate-800 flex justify-between text-xs items-center bg-slate-950/50">
|
|
<div class="flex gap-4 items-center">
|
|
<div class="font-mono">
|
|
Status: <span :class="['font-bold', statusColor]">{{ store.activeRequest.response.status }}</span>
|
|
</div>
|
|
<div class="text-slate-500">
|
|
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
|
|
</div>
|
|
<div class="text-slate-500">
|
|
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<div class="flex rounded-lg border border-slate-800 bg-slate-900 p-0.5">
|
|
<button
|
|
@click="activeTab = 'body'"
|
|
class="rounded-md px-2.5 py-1 transition-colors"
|
|
:class="activeTab === 'body' ? 'bg-indigo-500/15 text-indigo-300' : 'text-slate-400 hover:text-slate-200'"
|
|
>
|
|
Body
|
|
</button>
|
|
<button
|
|
@click="activeTab = 'headers'"
|
|
class="rounded-md px-2.5 py-1 transition-colors"
|
|
:class="activeTab === 'headers' ? 'bg-indigo-500/15 text-indigo-300' : 'text-slate-400 hover:text-slate-200'"
|
|
>
|
|
Headers
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
@click="copyToClipboard"
|
|
class="p-1.5 text-slate-400 hover:text-indigo-400 hover:bg-indigo-500/10 rounded transition-colors"
|
|
:title="activeTab === 'headers' ? 'Copy Headers' : 'Copy Body'"
|
|
>
|
|
<Check v-if="isCopied" class="w-3.5 h-3.5" />
|
|
<Clipboard v-else class="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Output -->
|
|
<div class="flex-1 overflow-hidden">
|
|
<CodeEditor
|
|
:model-value="displayedContent"
|
|
:language="activeTab === 'body' && isJsonResponse ? 'json' : 'text'"
|
|
:read-only="true"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="store.requestError" class="flex-1 flex items-center justify-center p-6">
|
|
<div class="max-w-2xl rounded-xl border border-rose-500/30 bg-rose-500/10 p-4 text-sm text-rose-200">
|
|
{{ store.requestError }}
|
|
</div>
|
|
</div>
|
|
<div v-else class="flex-1 flex items-center justify-center text-slate-600 text-sm">
|
|
No response yet
|
|
</div>
|
|
</div>
|
|
</template>
|