|
|
@@ -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 `<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 handleCopy = async () => {
|
|
|
- try {
|
|
|
- let textToCopy = content;
|
|
|
+ const formattedContent = useMemo(() => formatContent(content), [content]);
|
|
|
|
|
|
- // 如果是对象,转换为格式化的 JSON 字符串
|
|
|
- if (typeof content === 'object' && content !== null) {
|
|
|
- textToCopy = JSON.stringify(content, null, 2);
|
|
|
- }
|
|
|
+ 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 success = await copy(textToCopy);
|
|
|
- if (success) {
|
|
|
- setCopied(true);
|
|
|
- Toast.success(t('已复制到剪贴板'));
|
|
|
- setTimeout(() => setCopied(false), 2000);
|
|
|
- } else {
|
|
|
- Toast.error(t('复制失败'));
|
|
|
- }
|
|
|
- } catch (err) {
|
|
|
- Toast.error(t('复制失败'));
|
|
|
- console.error('Copy failed:', err);
|
|
|
+ const displayContent = useMemo(() => {
|
|
|
+ if (!contentMetrics.isLarge || isExpanded) {
|
|
|
+ return formattedContent;
|
|
|
}
|
|
|
- };
|
|
|
+ return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
|
|
|
+ '\n\n// ... 内容被截断以提升性能 ...';
|
|
|
+ }, [formattedContent, contentMetrics.isLarge, isExpanded]);
|
|
|
|
|
|
- // 格式化内容
|
|
|
- const getFormattedContent = () => {
|
|
|
- if (!content) return '';
|
|
|
+ const highlightedContent = useMemo(() => {
|
|
|
+ if (contentMetrics.isVeryLarge && !isExpanded) {
|
|
|
+ return displayContent;
|
|
|
+ }
|
|
|
|
|
|
- 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;
|
|
|
- }
|
|
|
+ if (isJsonLike(displayContent, language)) {
|
|
|
+ return highlightJson(displayContent);
|
|
|
}
|
|
|
|
|
|
- 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 displayContent;
|
|
|
+ }, [displayContent, language, contentMetrics.isVeryLarge, isExpanded]);
|
|
|
+
|
|
|
+ const handleCopy = useCallback(async () => {
|
|
|
+ try {
|
|
|
+ const textToCopy = typeof content === 'object' && content !== null
|
|
|
+ ? JSON.stringify(content, null, 2)
|
|
|
+ : content;
|
|
|
|
|
|
- return looksLikeJson;
|
|
|
- };
|
|
|
+ const success = await copy(textToCopy);
|
|
|
+ setCopied(true);
|
|
|
+ Toast.success(t('已复制到剪贴板'));
|
|
|
+ setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
|
- if (isJsonLike()) {
|
|
|
- return highlightJson(formattedContent);
|
|
|
+ if (!success) {
|
|
|
+ throw new Error('Copy operation failed');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ Toast.error(t('复制失败'));
|
|
|
+ console.error('Copy failed:', err);
|
|
|
}
|
|
|
+ }, [content, t]);
|
|
|
|
|
|
- // 对于非 JSON 内容,使用简单的文本高亮
|
|
|
- return formattedContent;
|
|
|
- };
|
|
|
+ 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>
|
|
|
- {title === 'preview' ? t('正在构造请求体预览...') :
|
|
|
- title === 'request' ? t('暂无请求数据') :
|
|
|
- t('暂无响应数据')}
|
|
|
- </span>
|
|
|
+ <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.copyButton,
|
|
|
- ...(isHoveringCopy ? codeThemeStyles.copyButtonHover : {})
|
|
|
+ ...codeThemeStyles.actionButton,
|
|
|
+ ...(isHoveringCopy ? codeThemeStyles.actionButtonHover : {}),
|
|
|
+ top: warningTop,
|
|
|
+ right: '12px',
|
|
|
}}
|
|
|
onMouseEnter={() => setIsHoveringCopy(true)}
|
|
|
onMouseLeave={() => setIsHoveringCopy(false)}
|
|
|
@@ -206,10 +245,67 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|
|
|
|
|
{/* 代码内容 */}
|
|
|
<div
|
|
|
- style={codeThemeStyles.content}
|
|
|
+ style={{
|
|
|
+ ...codeThemeStyles.content,
|
|
|
+ paddingTop: contentPadding,
|
|
|
+ }}
|
|
|
className="model-settings-scroll"
|
|
|
- dangerouslySetInnerHTML={{ __html: getHighlightedContent() }}
|
|
|
- />
|
|
|
+ >
|
|
|
+ {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>
|
|
|
);
|
|
|
};
|