|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|