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>