| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useState, useMemo, useCallback } from 'react'; |
| import { Button, Tooltip, Toast } from '@douyinfe/semi-ui'; |
| import { Copy, ChevronDown, ChevronUp } from 'lucide-react'; |
| import { useTranslation } from 'react-i18next'; |
| import { copy } from '../../helpers'; |
|
|
| const PERFORMANCE_CONFIG = { |
| MAX_DISPLAY_LENGTH: 50000, |
| PREVIEW_LENGTH: 5000, |
| VERY_LARGE_MULTIPLIER: 2, |
| }; |
|
|
| const codeThemeStyles = { |
| container: { |
| backgroundColor: '#1e1e1e', |
| color: '#d4d4d4', |
| fontFamily: 'Consolas, "Courier New", Monaco, "SF Mono", monospace', |
| fontSize: '13px', |
| lineHeight: '1.4', |
| borderRadius: '8px', |
| border: '1px solid #3c3c3c', |
| position: 'relative', |
| overflow: 'hidden', |
| boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', |
| }, |
| content: { |
| height: '100%', |
| overflowY: 'auto', |
| overflowX: 'auto', |
| padding: '16px', |
| margin: 0, |
| whiteSpace: 'pre', |
| wordBreak: 'normal', |
| background: '#1e1e1e', |
| }, |
| actionButton: { |
| position: 'absolute', |
| zIndex: 10, |
| backgroundColor: 'rgba(45, 45, 45, 0.9)', |
| border: '1px solid rgba(255, 255, 255, 0.1)', |
| color: '#d4d4d4', |
| borderRadius: '6px', |
| transition: 'all 0.2s ease', |
| }, |
| actionButtonHover: { |
| backgroundColor: 'rgba(60, 60, 60, 0.95)', |
| borderColor: 'rgba(255, 255, 255, 0.2)', |
| transform: 'scale(1.05)', |
| }, |
| noContent: { |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| height: '100%', |
| color: '#666', |
| fontSize: '14px', |
| 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', |
| }, |
| }; |
|
|
| 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'; |
| if (/^"/.test(match)) { |
| color = /:$/.test(match) ? '#9cdcfe' : '#ce9178'; |
| } else if (/true|false|null/.test(match)) { |
| color = '#569cd6'; |
| } |
| return `<span style="color: ${color}">${match}</span>`; |
| }, |
| ); |
| }; |
|
|
| 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 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 { |
| const textToCopy = |
| typeof content === 'object' && content !== null |
| ? JSON.stringify(content, null, 2) |
| : content; |
|
|
| const success = await copy(textToCopy); |
| 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 handleToggleExpand = useCallback(() => { |
| if (contentMetrics.isVeryLarge && !isExpanded) { |
| setIsProcessing(true); |
| setTimeout(() => { |
| setIsExpanded(true); |
| setIsProcessing(false); |
| }, 100); |
| } else { |
| setIsExpanded(!isExpanded); |
| } |
| }, [isExpanded, contentMetrics.isVeryLarge]); |
|
|
| if (!content) { |
| const placeholderText = |
| { |
| preview: t('正在构造请求体预览...'), |
| request: t('暂无请求数据'), |
| response: t('暂无响应数据'), |
| }[title] || t('暂无数据'); |
|
|
| return ( |
| <div style={codeThemeStyles.noContent}> |
| <span>{placeholderText}</span> |
| </div> |
| ); |
| } |
|
|
| const warningTop = contentMetrics.isLarge ? '52px' : '12px'; |
| const contentPadding = contentMetrics.isLarge ? '52px' : '16px'; |
|
|
| return ( |
| <div style={codeThemeStyles.container} className='h-full'> |
| {/* 性能警告 */} |
| {contentMetrics.isLarge && ( |
| <div style={codeThemeStyles.performanceWarning}> |
| <span>⚡</span> |
| <span> |
| {contentMetrics.isVeryLarge |
| ? t('内容较大,已启用性能优化模式') |
| : t('内容较大,部分功能可能受限')} |
| </span> |
| </div> |
| )} |
| |
| {/* 复制按钮 */} |
| <div |
| style={{ |
| ...codeThemeStyles.actionButton, |
| ...(isHoveringCopy ? codeThemeStyles.actionButtonHover : {}), |
| top: warningTop, |
| right: '12px', |
| }} |
| onMouseEnter={() => setIsHoveringCopy(true)} |
| onMouseLeave={() => setIsHoveringCopy(false)} |
| > |
| <Tooltip content={copied ? t('已复制') : t('复制代码')}> |
| <Button |
| icon={<Copy size={14} />} |
| onClick={handleCopy} |
| size='small' |
| theme='borderless' |
| style={{ |
| backgroundColor: 'transparent', |
| border: 'none', |
| color: copied ? '#4ade80' : '#d4d4d4', |
| padding: '6px', |
| }} |
| /> |
| </Tooltip> |
| </div> |
| |
| {/* 代码内容 */} |
| <div |
| style={{ |
| ...codeThemeStyles.content, |
| paddingTop: contentPadding, |
| }} |
| className='model-settings-scroll' |
| > |
| {isProcessing ? ( |
| <div |
| style={{ |
| display: 'flex', |
| alignItems: 'center', |
| justifyContent: 'center', |
| height: '200px', |
| color: '#888', |
| }} |
| > |
| <div |
| style={{ |
| width: '20px', |
| height: '20px', |
| border: '2px solid #444', |
| borderTop: '2px solid #888', |
| borderRadius: '50%', |
| animation: 'spin 1s linear infinite', |
| marginRight: '8px', |
| }} |
| /> |
| {t('正在处理大内容...')} |
| </div> |
| ) : ( |
| <div dangerouslySetInnerHTML={{ __html: highlightedContent }} /> |
| )} |
| </div> |
| |
| {/* 展开/收起按钮 */} |
| {contentMetrics.isLarge && !isProcessing && ( |
| <div |
| style={{ |
| ...codeThemeStyles.actionButton, |
| bottom: '12px', |
| left: '50%', |
| transform: 'translateX(-50%)', |
| }} |
| > |
| <Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}> |
| <Button |
| icon={ |
| isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} /> |
| } |
| onClick={handleToggleExpand} |
| size='small' |
| theme='borderless' |
| style={{ |
| backgroundColor: 'transparent', |
| border: 'none', |
| color: '#d4d4d4', |
| padding: '6px 12px', |
| }} |
| > |
| {isExpanded ? t('收起') : t('展开')} |
| {!isExpanded && ( |
| <span |
| style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }} |
| > |
| (+ |
| {Math.round( |
| (contentMetrics.length - |
| PERFORMANCE_CONFIG.PREVIEW_LENGTH) / |
| 1000, |
| )} |
| K) |
| </span> |
| )} |
| </Button> |
| </Tooltip> |
| </div> |
| )} |
| </div> |
| ); |
| }; |
|
|
| export default CodeViewer; |
|
|