| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | 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 linkRegex = /(https?:\/\/[^\s<"'\]),;}]+)/g;
|
| |
|
| | const linkifyHtml = (html) => {
|
| | const parts = html.split(/(<[^>]+>)/g);
|
| | return parts
|
| | .map((part) => {
|
| | if (part.startsWith('<')) return part;
|
| | return part.replace(
|
| | linkRegex,
|
| | (url) => `<a href="${url}" target="_blank" rel="noreferrer">${url}</a>`,
|
| | );
|
| | })
|
| | .join('');
|
| | };
|
| |
|
| | 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 renderedContent = useMemo(() => {
|
| | return linkifyHtml(highlightedContent);
|
| | }, [highlightedContent]);
|
| |
|
| | 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,
|
| | whiteSpace: 'pre-wrap',
|
| | wordBreak: 'break-word',
|
| | }}
|
| | 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: renderedContent }} />
|
| | )}
|
| | </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;
|
| |
|