✨ feat: enhance debug panel with VS Code dark theme and syntax highlighting
- Create new CodeViewer component with VS Code dark theme styling - Implement custom JSON syntax highlighting with proper color coding - Add improved copy functionality with hover effects and user feedback - Refactor DebugPanel to use the new CodeViewer component - Apply dark background (#1e1e1e) with syntax colors matching VS Code - Enhance UX with floating copy button and responsive design - Support automatic JSON formatting and parsing - Maintain compatibility with existing Semi Design components The debug panel now displays preview requests, actual requests, and responses in a professional code editor style, improving readability and developer experience in the playground interface.
This commit is contained in:
194
web/src/components/playground/CodeViewer.js
Normal file
194
web/src/components/playground/CodeViewer.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { Copy } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copy } from '../../helpers/utils';
|
||||
|
||||
// VS Code 深色主题样式
|
||||
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',
|
||||
},
|
||||
copyButton: {
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
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',
|
||||
},
|
||||
copyButtonHover: {
|
||||
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',
|
||||
}
|
||||
};
|
||||
|
||||
// 自定义 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'; // 数字颜色 (绿色)
|
||||
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 值颜色 (蓝色)
|
||||
}
|
||||
return `<span style="color: ${color}">${match}</span>`;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const CodeViewer = ({ content, title, language = 'json' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isHoveringCopy, setIsHoveringCopy] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
let textToCopy = content;
|
||||
|
||||
// 如果是对象,转换为格式化的 JSON 字符串
|
||||
if (typeof content === 'object' && content !== null) {
|
||||
textToCopy = JSON.stringify(content, null, 2);
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
return String(content);
|
||||
};
|
||||
|
||||
// 获取高亮的 HTML
|
||||
const getHighlightedContent = () => {
|
||||
const formattedContent = getFormattedContent();
|
||||
|
||||
if (language === 'json') {
|
||||
return highlightJson(formattedContent);
|
||||
}
|
||||
|
||||
// 对于非 JSON 内容,使用简单的文本高亮
|
||||
return formattedContent;
|
||||
};
|
||||
|
||||
if (!content) {
|
||||
return (
|
||||
<div style={codeThemeStyles.noContent}>
|
||||
<span>
|
||||
{title === 'preview' ? t('正在构造请求体预览...') :
|
||||
title === 'request' ? t('暂无请求数据') :
|
||||
t('暂无响应数据')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={codeThemeStyles.container} className="h-full">
|
||||
{/* 复制按钮 */}
|
||||
<div
|
||||
style={{
|
||||
...codeThemeStyles.copyButton,
|
||||
...(isHoveringCopy ? codeThemeStyles.copyButtonHover : {})
|
||||
}}
|
||||
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}
|
||||
className="model-settings-scroll"
|
||||
dangerouslySetInnerHTML={{ __html: getHighlightedContent() }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeViewer;
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CodeViewer from './CodeViewer';
|
||||
|
||||
const DebugPanel = ({
|
||||
debugData,
|
||||
@@ -129,17 +130,11 @@ const DebugPanel = ({
|
||||
{t('预览请求体')}
|
||||
</div>
|
||||
} itemKey="preview">
|
||||
<div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
||||
{debugData.previewRequest ? (
|
||||
<pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(debugData.previewRequest, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{t('正在构造请求体预览...')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<CodeViewer
|
||||
content={debugData.previewRequest}
|
||||
title="preview"
|
||||
language="json"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={
|
||||
@@ -148,19 +143,11 @@ const DebugPanel = ({
|
||||
{t('实际请求体')}
|
||||
</div>
|
||||
} itemKey="request">
|
||||
<div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
||||
{debugData.request ? (
|
||||
<>
|
||||
<pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(debugData.request, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{t('暂无请求数据')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<CodeViewer
|
||||
content={debugData.request}
|
||||
title="request"
|
||||
language="json"
|
||||
/>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={
|
||||
@@ -169,17 +156,11 @@ const DebugPanel = ({
|
||||
{t('响应内容')}
|
||||
</div>
|
||||
} itemKey="response">
|
||||
<div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
||||
{debugData.response ? (
|
||||
<pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
||||
{debugData.response}
|
||||
</pre>
|
||||
) : (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{t('暂无响应数据')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
<CodeViewer
|
||||
content={debugData.response}
|
||||
title="response"
|
||||
language="javascript"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user