436 lines
16 KiB
Vue
436 lines
16 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- Enhanced Body Type Selector -->
|
|
<div class="flex items-center gap-4">
|
|
<FormKit
|
|
type="select"
|
|
v-model="body.type"
|
|
:options="[
|
|
{ value: 'none', label: 'None' },
|
|
{ value: 'raw', label: 'Raw' },
|
|
{ value: 'form-data', label: 'Form-data' },
|
|
{ value: 'x-www-form-urlencoded', label: 'x-www-form-urlencoded' }
|
|
]"
|
|
outer-class="!mb-0 max-w-md"
|
|
inner-class="!mb-0 font-medium"
|
|
/>
|
|
<div v-if="body.type !== 'none'" class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ getBodyTypeDescription() }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Raw Body with Enhanced JSON Tools -->
|
|
<div v-if="body.type === 'raw'" class="space-y-4">
|
|
<!-- Enhanced Content Type Selector -->
|
|
<div class="flex items-center gap-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border">
|
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 min-w-fit">Content Type:</label>
|
|
<FormKit
|
|
type="select"
|
|
v-model="rawContentType"
|
|
:options="[
|
|
{ value: 'application/json', label: 'JSON' },
|
|
{ value: 'text/plain', label: 'Text' },
|
|
{ value: 'application/xml', label: 'XML' },
|
|
{ value: 'text/html', label: 'HTML' },
|
|
{ value: 'application/javascript', label: 'JavaScript' },
|
|
{ value: 'text/css', label: 'CSS' }
|
|
]"
|
|
outer-class="!mb-0 flex-1"
|
|
inner-class="!mb-0"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Enhanced JSON Tools -->
|
|
<div v-if="rawContentType === 'application/json'" class="flex items-center justify-between p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<div class="flex items-center gap-2">
|
|
<Icon name="ic:outline-data-object" size="16" class="text-blue-600 dark:text-blue-400" />
|
|
<span class="text-sm font-medium text-blue-900 dark:text-blue-100">JSON Tools</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<rs-button variant="secondary-outline" size="sm" @click="beautifyJson">
|
|
<Icon name="ic:outline-auto-fix-high" size="16" class="mr-1" />
|
|
Beautify
|
|
</rs-button>
|
|
<rs-button variant="secondary-outline" size="sm" @click="minifyJson">
|
|
<Icon name="ic:outline-compress" size="16" class="mr-1" />
|
|
Minify
|
|
</rs-button>
|
|
<rs-button variant="secondary-outline" size="sm" @click="copyToClipboard">
|
|
<Icon name="ic:outline-content-copy" size="16" class="mr-1" />
|
|
Copy
|
|
</rs-button>
|
|
<rs-button variant="secondary-outline" size="sm" @click="clearContent">
|
|
<Icon name="ic:outline-clear" size="16" class="mr-1" />
|
|
Clear
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enhanced Textarea with better styling -->
|
|
<div class="relative">
|
|
<FormKit
|
|
type="textarea"
|
|
v-model="body.raw"
|
|
:placeholder="getPlaceholderText()"
|
|
rows="16"
|
|
outer-class="!mb-0"
|
|
inner-class="!mb-0 font-mono text-sm resize-none leading-relaxed"
|
|
@input="validateJson"
|
|
/>
|
|
|
|
<!-- JSON Validation Status -->
|
|
<div v-if="rawContentType === 'application/json' && body.raw" class="absolute top-3 right-3 flex items-center gap-2">
|
|
<div v-if="jsonValidation.isValid" class="flex items-center gap-1 text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20 px-2 py-1 rounded text-xs">
|
|
<Icon name="ic:outline-check-circle" size="14" />
|
|
<span>Valid JSON</span>
|
|
</div>
|
|
<div v-else-if="jsonValidation.error" class="flex items-center gap-1 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 px-2 py-1 rounded text-xs">
|
|
<Icon name="ic:outline-error" size="14" />
|
|
<span>Invalid JSON</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enhanced JSON Error Display -->
|
|
<div v-if="rawContentType === 'application/json' && jsonValidation.error" class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<Icon name="ic:outline-error" size="16" class="text-red-600 dark:text-red-400" />
|
|
<div class="font-medium text-red-900 dark:text-red-100">JSON Syntax Error</div>
|
|
</div>
|
|
<div class="text-sm text-red-700 dark:text-red-300 font-mono bg-red-100 dark:bg-red-900/40 p-2 rounded">
|
|
{{ jsonValidation.error }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enhanced Form-data Body -->
|
|
<div v-if="body.type === 'form-data'" class="space-y-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">Form Data Fields</h4>
|
|
<rs-button variant="primary" size="sm" @click="addFormDataRow">
|
|
<Icon name="ic:outline-add" size="16" class="mr-1" />
|
|
Add Field
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(item, index) in body.formData" :key="index" class="group">
|
|
<div class="flex items-start gap-3 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors">
|
|
<!-- Active checkbox -->
|
|
<input
|
|
type="checkbox"
|
|
v-model="item.active"
|
|
class="w-4 h-4 mt-3 flex-shrink-0 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
|
/>
|
|
|
|
<div class="flex-1 space-y-3 min-w-0">
|
|
<!-- Key and Type Row -->
|
|
<div class="flex items-center gap-3">
|
|
<FormKit
|
|
type="text"
|
|
v-model="item.key"
|
|
placeholder="Field name"
|
|
outer-class="!mb-0 flex-1"
|
|
inner-class="!mb-0"
|
|
/>
|
|
<FormKit
|
|
type="select"
|
|
v-model="item.type"
|
|
:options="[
|
|
{ value: 'text', label: 'Text' },
|
|
{ value: 'file', label: 'File' }
|
|
]"
|
|
outer-class="!mb-0 w-24"
|
|
inner-class="!mb-0"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Value input (for text) -->
|
|
<FormKit
|
|
v-if="item.type === 'text'"
|
|
type="text"
|
|
v-model="item.value"
|
|
placeholder="Field value"
|
|
outer-class="!mb-0"
|
|
inner-class="!mb-0"
|
|
/>
|
|
|
|
<!-- Enhanced File input (for file) -->
|
|
<div v-if="item.type === 'file'" class="space-y-3">
|
|
<div
|
|
@drop="handleFileDrop($event, index)"
|
|
@dragover.prevent
|
|
@dragenter.prevent
|
|
class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center hover:border-primary-400 dark:hover:border-primary-500 transition-colors cursor-pointer"
|
|
@click="$refs[`fileInput${index}`]?.[0]?.click()"
|
|
>
|
|
<input
|
|
:ref="`fileInput${index}`"
|
|
type="file"
|
|
@change="handleFileChange($event, index)"
|
|
class="hidden"
|
|
multiple
|
|
/>
|
|
<div v-if="!item.file">
|
|
<Icon name="ic:outline-cloud-upload" size="32" class="mx-auto mb-2 text-gray-400" />
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
|
Drop files here or click to browse
|
|
</p>
|
|
<p class="text-xs text-gray-500">Maximum file size: 10MB</p>
|
|
</div>
|
|
<div v-else class="flex items-center justify-center gap-3">
|
|
<Icon name="ic:outline-description" size="24" class="text-blue-500" />
|
|
<div class="text-left">
|
|
<div class="font-medium text-gray-900 dark:text-white">{{ item.file.name }}</div>
|
|
<div class="text-sm text-gray-500">{{ formatFileSize(item.file.size) }}</div>
|
|
</div>
|
|
<rs-button
|
|
variant="danger-outline"
|
|
size="sm"
|
|
@click.stop="removeFile(index)"
|
|
class="ml-auto"
|
|
>
|
|
<Icon name="ic:outline-delete" size="14" />
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<FormKit
|
|
type="text"
|
|
v-model="item.description"
|
|
placeholder="Field description (optional)"
|
|
outer-class="!mb-0"
|
|
inner-class="!mb-0 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<rs-button
|
|
variant="danger-outline"
|
|
size="sm"
|
|
@click="removeFormDataRow(index)"
|
|
class="flex-shrink-0 mt-3 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<Icon name="ic:outline-delete" size="16" />
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enhanced URL-encoded Body -->
|
|
<div v-if="body.type === 'x-www-form-urlencoded'" class="space-y-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="font-medium text-gray-900 dark:text-white">URL Encoded Parameters</h4>
|
|
<rs-button variant="primary" size="sm" @click="addRow">
|
|
<Icon name="ic:outline-add" size="16" class="mr-1" />
|
|
Add Parameter
|
|
</rs-button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(item, index) in body.urlEncoded" :key="index" class="group">
|
|
<div class="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 transition-colors">
|
|
<input
|
|
type="checkbox"
|
|
v-model="item.active"
|
|
class="w-4 h-4 flex-shrink-0 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
v-model="item.key"
|
|
placeholder="Parameter name"
|
|
outer-class="!mb-0 flex-1 min-w-0"
|
|
inner-class="!mb-0"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
v-model="item.value"
|
|
placeholder="Parameter value"
|
|
outer-class="!mb-0 flex-1 min-w-0"
|
|
inner-class="!mb-0"
|
|
/>
|
|
<rs-button
|
|
variant="danger-outline"
|
|
size="sm"
|
|
@click="removeRow(index)"
|
|
class="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<Icon name="ic:outline-delete" size="16" />
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const { addRow: addRowUtil, removeRow: removeRowUtil } = useApiRequest()
|
|
|
|
const props = defineProps({
|
|
body: {
|
|
type: Object,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
// Content type for raw body
|
|
const rawContentType = ref('application/json')
|
|
|
|
// JSON validation state
|
|
const jsonValidation = ref({
|
|
isValid: false,
|
|
error: null
|
|
})
|
|
|
|
// Get body type description
|
|
const getBodyTypeDescription = () => {
|
|
const descriptions = {
|
|
'raw': 'Send raw text data like JSON, XML, or plain text',
|
|
'form-data': 'Send multipart form data, supports file uploads',
|
|
'x-www-form-urlencoded': 'Send form data as URL-encoded key-value pairs'
|
|
}
|
|
return descriptions[props.body.type] || ''
|
|
}
|
|
|
|
// Validate JSON content
|
|
const validateJson = () => {
|
|
if (rawContentType.value !== 'application/json' || !props.body.raw) {
|
|
jsonValidation.value = { isValid: false, error: null }
|
|
return
|
|
}
|
|
|
|
try {
|
|
JSON.parse(props.body.raw)
|
|
jsonValidation.value = { isValid: true, error: null }
|
|
} catch (error) {
|
|
jsonValidation.value = { isValid: false, error: error.message }
|
|
}
|
|
}
|
|
|
|
// JSON Tools
|
|
const beautifyJson = () => {
|
|
try {
|
|
const parsed = JSON.parse(props.body.raw)
|
|
props.body.raw = JSON.stringify(parsed, null, 2)
|
|
validateJson()
|
|
} catch (error) {
|
|
// Show notification for invalid JSON
|
|
console.error('Cannot beautify invalid JSON:', error)
|
|
}
|
|
}
|
|
|
|
const minifyJson = () => {
|
|
try {
|
|
const parsed = JSON.parse(props.body.raw)
|
|
props.body.raw = JSON.stringify(parsed)
|
|
validateJson()
|
|
} catch (error) {
|
|
console.error('Cannot minify invalid JSON:', error)
|
|
}
|
|
}
|
|
|
|
const copyToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(props.body.raw)
|
|
// Could add notification here
|
|
} catch (error) {
|
|
console.error('Failed to copy to clipboard:', error)
|
|
}
|
|
}
|
|
|
|
const clearContent = () => {
|
|
props.body.raw = ''
|
|
validateJson()
|
|
}
|
|
|
|
// Placeholder text based on content type
|
|
const getPlaceholderText = () => {
|
|
switch (rawContentType.value) {
|
|
case 'application/json':
|
|
return '{\n "key": "value",\n "number": 123,\n "boolean": true,\n "array": [1, 2, 3],\n "object": {\n "nested": "data"\n }\n}'
|
|
case 'application/xml':
|
|
return '<?xml version="1.0" encoding="UTF-8"?>\n<root>\n <item id="1">\n <name>Example</name>\n <value>123</value>\n </item>\n</root>'
|
|
case 'text/html':
|
|
return '<!DOCTYPE html>\n<html>\n<head>\n <title>Example</title>\n</head>\n<body>\n <h1>Hello World</h1>\n <p>This is an example HTML document.</p>\n</body>\n</html>'
|
|
case 'application/javascript':
|
|
return 'const data = {\n message: "Hello World",\n timestamp: new Date().toISOString()\n};\n\nconsole.log(data);'
|
|
case 'text/css':
|
|
return 'body {\n font-family: Arial, sans-serif;\n margin: 0;\n padding: 20px;\n}\n\n.container {\n max-width: 1200px;\n margin: 0 auto;\n}'
|
|
default:
|
|
return 'Enter your request body content here...'
|
|
}
|
|
}
|
|
|
|
// Enhanced form-data handling
|
|
const addFormDataRow = () => {
|
|
props.body.formData.push({
|
|
active: true,
|
|
key: '',
|
|
value: '',
|
|
description: '',
|
|
type: 'text',
|
|
file: null
|
|
})
|
|
}
|
|
|
|
const removeFormDataRow = (index) => {
|
|
if (props.body.formData.length > 1) {
|
|
props.body.formData.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
const handleFileChange = (event, index) => {
|
|
const file = event.target.files[0]
|
|
if (file) {
|
|
if (file.size > 10 * 1024 * 1024) { // 10MB limit
|
|
alert('File size must be less than 10MB')
|
|
return
|
|
}
|
|
props.body.formData[index].file = file
|
|
props.body.formData[index].value = file.name
|
|
}
|
|
}
|
|
|
|
const handleFileDrop = (event, index) => {
|
|
event.preventDefault()
|
|
const files = Array.from(event.dataTransfer.files)
|
|
if (files.length > 0) {
|
|
const file = files[0]
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
alert('File size must be less than 10MB')
|
|
return
|
|
}
|
|
props.body.formData[index].file = file
|
|
props.body.formData[index].value = file.name
|
|
}
|
|
}
|
|
|
|
const removeFile = (index) => {
|
|
props.body.formData[index].file = null
|
|
props.body.formData[index].value = ''
|
|
}
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (bytes === 0) return '0 Bytes'
|
|
const k = 1024
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
}
|
|
|
|
// URL-encoded handling
|
|
const addRow = () => {
|
|
addRowUtil(toRef(props.body, 'urlEncoded'))
|
|
}
|
|
|
|
const removeRow = (index) => {
|
|
removeRowUtil(toRef(props.body, 'urlEncoded'), index)
|
|
}
|
|
|
|
// Watch for content type changes to validate JSON
|
|
watch(rawContentType, validateJson)
|
|
watch(() => props.body.raw, validateJson, { immediate: true })
|
|
</script> |