Просмотр исходного кода

✨ 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.
Apple\Apple 9 месяцев назад
Родитель
Сommit
6242cc31f2
2 измененных файлов с 210 добавлено и 35 удалено
  1. 194 0
      web/src/components/playground/CodeViewer.js
  2. 16 35
      web/src/components/playground/DebugPanel.js

+ 194 - 0
web/src/components/playground/CodeViewer.js

@@ -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 - 35
web/src/components/playground/DebugPanel.js

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