diff --git a/web/src/components/playground/CodeViewer.js b/web/src/components/playground/CodeViewer.js index 2a8fc405..e02b72e2 100644 --- a/web/src/components/playground/CodeViewer.js +++ b/web/src/components/playground/CodeViewer.js @@ -1,10 +1,15 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { Button, Tooltip, Toast } from '@douyinfe/semi-ui'; -import { Copy } from 'lucide-react'; +import { Copy, ChevronDown, ChevronUp } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { copy } from '../../helpers/utils'; -// VS Code 深色主题样式 +const PERFORMANCE_CONFIG = { + MAX_DISPLAY_LENGTH: 50000, // 最大显示字符数 + PREVIEW_LENGTH: 5000, // 预览长度 + VERY_LARGE_MULTIPLIER: 2, // 超大内容倍数 +}; + const codeThemeStyles = { container: { backgroundColor: '#1e1e1e', @@ -28,10 +33,8 @@ const codeThemeStyles = { wordBreak: 'normal', background: '#1e1e1e', }, - copyButton: { + actionButton: { position: 'absolute', - top: '12px', - right: '12px', zIndex: 10, backgroundColor: 'rgba(45, 45, 45, 0.9)', border: '1px solid rgba(255, 255, 255, 0.1)', @@ -39,7 +42,7 @@ const codeThemeStyles = { borderRadius: '6px', transition: 'all 0.2s ease', }, - copyButtonHover: { + actionButtonHover: { backgroundColor: 'rgba(60, 60, 60, 0.95)', borderColor: 'rgba(255, 255, 255, 0.2)', transform: 'scale(1.05)', @@ -54,136 +57,172 @@ const codeThemeStyles = { fontStyle: 'italic', backgroundColor: 'var(--semi-color-fill-0)', borderRadius: '8px', - } + }, + performanceWarning: { + padding: '8px 12px', + backgroundColor: 'rgba(255, 193, 7, 0.1)', + border: '1px solid rgba(255, 193, 7, 0.3)', + borderRadius: '6px', + color: '#ffc107', + fontSize: '12px', + marginBottom: '8px', + display: 'flex', + alignItems: 'center', + gap: '8px', + }, }; -// 自定义 JSON 高亮器(使用 VS Code 深色主题配色) const highlightJson = (str) => { return str.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => { - let color = '#b5cea8'; // 数字颜色 (绿色) + let color = '#b5cea8'; if (/^"/.test(match)) { - if (/:$/.test(match)) { - color = '#9cdcfe'; // 键名颜色 (蓝色) - } else { - color = '#ce9178'; // 字符串值颜色 (橙色) - } - } else if (/true|false/.test(match)) { - color = '#569cd6'; // 布尔值颜色 (蓝色) - } else if (/null/.test(match)) { - color = '#569cd6'; // null 值颜色 (蓝色) + color = /:$/.test(match) ? '#9cdcfe' : '#ce9178'; + } else if (/true|false|null/.test(match)) { + color = '#569cd6'; } return `${match}`; } ); }; +const isJsonLike = (content, language) => { + if (language === 'json') return true; + const trimmed = content.trim(); + return (trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']')); +}; + +const formatContent = (content) => { + if (!content) return ''; + + if (typeof content === 'object') { + try { + return JSON.stringify(content, null, 2); + } catch (e) { + return String(content); + } + } + + if (typeof content === 'string') { + try { + const parsed = JSON.parse(content); + return JSON.stringify(parsed, null, 2); + } catch (e) { + return content; + } + } + + return String(content); +}; + const CodeViewer = ({ content, title, language = 'json' }) => { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const [isHoveringCopy, setIsHoveringCopy] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); - const handleCopy = async () => { + const formattedContent = useMemo(() => formatContent(content), [content]); + + const contentMetrics = useMemo(() => { + const length = formattedContent.length; + const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH; + const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER; + return { length, isLarge, isVeryLarge }; + }, [formattedContent.length]); + + const displayContent = useMemo(() => { + if (!contentMetrics.isLarge || isExpanded) { + return formattedContent; + } + return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) + + '\n\n// ... 内容被截断以提升性能 ...'; + }, [formattedContent, contentMetrics.isLarge, isExpanded]); + + const highlightedContent = useMemo(() => { + if (contentMetrics.isVeryLarge && !isExpanded) { + return displayContent; + } + + if (isJsonLike(displayContent, language)) { + return highlightJson(displayContent); + } + + return displayContent; + }, [displayContent, language, contentMetrics.isVeryLarge, isExpanded]); + + const handleCopy = useCallback(async () => { try { - let textToCopy = content; - - // 如果是对象,转换为格式化的 JSON 字符串 - if (typeof content === 'object' && content !== null) { - textToCopy = JSON.stringify(content, null, 2); - } + const textToCopy = typeof content === 'object' && content !== null + ? JSON.stringify(content, null, 2) + : content; const success = await copy(textToCopy); - if (success) { - setCopied(true); - Toast.success(t('已复制到剪贴板')); - setTimeout(() => setCopied(false), 2000); - } else { - Toast.error(t('复制失败')); + setCopied(true); + Toast.success(t('已复制到剪贴板')); + setTimeout(() => setCopied(false), 2000); + + if (!success) { + throw new Error('Copy operation failed'); } } catch (err) { Toast.error(t('复制失败')); console.error('Copy failed:', err); } - }; + }, [content, t]); - // 格式化内容 - const getFormattedContent = () => { - if (!content) return ''; - - if (typeof content === 'object') { - try { - return JSON.stringify(content, null, 2); - } catch (e) { - return String(content); - } - } else if (typeof content === 'string') { - // 尝试解析并重新格式化 JSON - try { - const parsed = JSON.parse(content); - return JSON.stringify(parsed, null, 2); - } catch (e) { - return content; - } + const handleToggleExpand = useCallback(() => { + if (contentMetrics.isVeryLarge && !isExpanded) { + setIsProcessing(true); + setTimeout(() => { + setIsExpanded(true); + setIsProcessing(false); + }, 100); + } else { + setIsExpanded(!isExpanded); } - - return String(content); - }; - - // 获取高亮的 HTML - const getHighlightedContent = () => { - const formattedContent = getFormattedContent(); - - // 尝试检测是否为 JSON 格式 - const isJsonLike = () => { - if (language === 'json') return true; - - // 自动检测:如果内容看起来像 JSON,就用 JSON 高亮 - const trimmed = formattedContent.trim(); - const looksLikeJson = (trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']')); - - // 调试日志 - if (process.env.NODE_ENV === 'development') { - console.log('CodeViewer Debug:', { - language, - contentType: typeof content, - trimmedStart: trimmed.substring(0, 10), - looksLikeJson, - willHighlight: looksLikeJson - }); - } - - return looksLikeJson; - }; - - if (isJsonLike()) { - return highlightJson(formattedContent); - } - - // 对于非 JSON 内容,使用简单的文本高亮 - return formattedContent; - }; + }, [isExpanded, contentMetrics.isVeryLarge]); if (!content) { + const placeholderText = { + preview: t('正在构造请求体预览...'), + request: t('暂无请求数据'), + response: t('暂无响应数据') + }[title] || t('暂无数据'); + return (