import React, { useState, useEffect, useRef } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGlobe, faCopy, faCheck, faFileAlt, faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import styles from '../styles'; import { HttpResponse, HttpMethod, NetworkType } from '../types'; import JsonRenderer from './JsonRenderer'; import { generateMarkdownDoc } from '../utils'; import MarkdownPreview from './MarkdownPreview'; import { useLanguage } from '@/context/LanguageContext'; interface ResponseDisplayProps { response: HttpResponse | null; url: string; method: HttpMethod; headers: {key: string; value: string; id: string}[]; body: string; bodyFormat: 'json' | 'text' | 'form'; formFields: {key: string; value: string; id: string}[]; networkType: NetworkType; } const ResponseDisplay: React.FC = ({ response, url, method, headers, body, bodyFormat, formFields, networkType }) => { const { t } = useLanguage(); const [responseTab, setResponseTab] = useState<'body' | 'headers' | 'info'>('body'); const [copiedJson, setCopiedJson] = useState(false); const [markdownContent, setMarkdownContent] = useState(''); const [showMarkdownPreview, setShowMarkdownPreview] = useState(false); const containerRef = useRef(null); const contentRef = useRef(null); // 美化JSON const formatJson = (json: string): string => { try { return JSON.stringify(JSON.parse(json), null, 2); } catch { return json; } }; // 检测内容是否为JSON const isJsonContent = (data: unknown): boolean => { // 检查响应头中的Content-Type if (response?.headers && response.headers['content-type']?.includes('application/json')) { return true; } // 对于对象类型的数据,直接判定为JSON if (typeof data === 'object' && data !== null) { return true; } // 尝试解析字符串 if (typeof data === 'string') { try { JSON.parse(data); return true; } catch { return false; } } return false; }; // 复制响应内容 const copyResponse = () => { if (!response) return; const textToCopy = responseTab === 'body' ? typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2) : Object.entries(response.headers) .map(([key, value]) => `${key}: ${value}`) .join('\n'); navigator.clipboard.writeText(textToCopy) .then(() => { setCopiedJson(true); setTimeout(() => setCopiedJson(false), 2000); }) .catch(err => console.error(t('tools.http_tester.copy_failed'), err)); }; // 生成并显示Markdown接口文档 const handleGenerateMarkdown = () => { if (!response) return; const markdownDoc = generateMarkdownDoc( url, method, headers, body, bodyFormat, formFields, response, networkType ); setMarkdownContent(markdownDoc); setShowMarkdownPreview(true); }; // 关闭Markdown预览窗口 const handleCloseMarkdownPreview = () => { setShowMarkdownPreview(false); }; // 调整响应区域高度以适应可用空间并跟随内容变化 useEffect(() => { if (!response) return; const adjustHeight = () => { if (!containerRef.current || !contentRef.current) return; // 计算可用的视窗高度 const viewportHeight = window.innerHeight; // 获取容器到视窗顶部的距离 const containerTop = containerRef.current.getBoundingClientRect().top; // 设置底部边距 const bottomMargin = 40; // 计算容器可用的最大高度(视口高度限制) const maxViewportHeight = viewportHeight - containerTop - bottomMargin; // 获取内容实际高度 const contentHeight = contentRef.current.scrollHeight; // 设置容器初始高度为视口可用高度 let targetHeight = Math.max(600, maxViewportHeight); // 如果内容高度超过初始高度,则让容器跟随内容增高 // 最小高度600px,最大不超过内容高度+100px(为头部和边距预留空间) if (contentHeight > targetHeight - 100) { targetHeight = Math.min(contentHeight + 100, 2000); // 设置一个最大值2000px,防止过长 } // 应用高度 containerRef.current.style.minHeight = `${targetHeight}px`; }; // 初始调整 adjustHeight(); // 设置一个延时调整,确保内容渲染完成后再次计算高度 const timeoutId = setTimeout(adjustHeight, 100); // 监听窗口大小变化 window.addEventListener('resize', adjustHeight); return () => { window.removeEventListener('resize', adjustHeight); clearTimeout(timeoutId); }; }, [response, responseTab]); return (

{t('tools.http_tester.response_result')}

{response ? (
{/* 响应头部 */}
{response.status} {response.statusText}
{response.size} bytes | {response.time}ms
{/* 响应标签页 */}
{/* 响应内容 */}
{responseTab === 'body' && ( <> {isJsonContent(response.data) ? : (typeof response.data === 'string' ? response.data : formatJson(JSON.stringify(response.data)))} )} {responseTab === 'headers' && ( Object.entries(response.headers).map(([key, value]) => (
{key}: {value}
)) )} {responseTab === 'info' && (
{t('tools.http_tester.network_mode')}: {networkType === 'local' ? t('tools.http_tester.network_mode_local') : t('tools.http_tester.network_mode_public')} {networkType === 'local' && ( {t('tools.http_tester.cors_description')} )}
{t('tools.http_tester.request_url')}: {url}
{t('tools.http_tester.request_method')}: {method}
{t('tools.http_tester.response_result')}: {response.time}ms
{t('tools.http_tester.response_result')}: {response.size} bytes
{Object.entries(headers).length > 0 && (
{t('tools.http_tester.request_headers')}
{headers.filter(h => h.key && h.value).map((header, index) => (
{header.key}: {header.value}
))}
)} {['POST', 'PUT', 'PATCH'].includes(method) && (
{t('tools.http_tester.request_body')}
{bodyFormat === 'json' ? formatJson(body) : bodyFormat === 'form' ? formFields.filter(f => f.key && f.value) .map((field, _index) => `${field.key}=${field.value}`) .join('&') : body}
)}
)}
) : (

{t('tools.http_tester.enter_url')}

)} {/* Markdown预览模态窗口 */}
); }; export default ResponseDisplay;