Compare commits
3 Commits
470e34e7a8
...
2e5120dc72
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5120dc72 | ||
|
|
b3d5a47072 | ||
|
|
4eab17155e |
@ -490,7 +490,7 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
|
|
||||||
{/* 固定底部输入框 */}
|
{/* 固定底部输入框 */}
|
||||||
<div
|
<div
|
||||||
className="sticky bottom-0 pt-4"
|
className="sticky bottom-0 pt-4 z-20"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to top, var(--color-bg-secondary) 0%, var(--color-bg-secondary) 80%, transparent 100%)`
|
background: `linear-gradient(to top, var(--color-bg-secondary) 0%, var(--color-bg-secondary) 80%, transparent 100%)`
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export function ChatInput({
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder={files.length > 0 ? '添加描述(可选)...' : placeholder}
|
placeholder={files.length > 0 ? '添加描述(可选)...' : placeholder}
|
||||||
className="w-full border-none outline-none text-base text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
|
className="w-full border-none outline-none text-[var(--color-text-primary)] bg-transparent py-2 placeholder:text-[var(--color-text-placeholder)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -152,7 +152,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-md text-base leading-relaxed">
|
<div className="bg-[var(--color-message-user)] text-[var(--color-text-primary)] px-4 py-3 rounded-md leading-relaxed">
|
||||||
{message.content || ((uploadedImages && uploadedImages.length > 0) || (uploadedDocuments && uploadedDocuments.length > 0) ? '(附件)' : '')}
|
{message.content || ((uploadedImages && uploadedImages.length > 0) || (uploadedDocuments && uploadedDocuments.length > 0) ? '(附件)' : '')}
|
||||||
</div>
|
</div>
|
||||||
{/* 悬停显示复制按钮 */}
|
{/* 悬停显示复制按钮 */}
|
||||||
@ -216,7 +216,7 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
</button>
|
</button>
|
||||||
{thinkingExpanded && (
|
{thinkingExpanded && (
|
||||||
<div className="mt-2 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
<div className="mt-2 p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
||||||
<pre className="text-sm text-purple-800 whitespace-pre-wrap font-mono">
|
<pre className="text-purple-800 whitespace-pre-wrap font-mono">
|
||||||
{thinkingContent}
|
{thinkingContent}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@ -228,13 +228,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="mb-3 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
|
<div className="mb-3 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
|
||||||
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
|
<AlertCircle size={20} className="text-red-500 flex-shrink-0 mt-0.5" />
|
||||||
<div className="text-sm text-red-700">{error}</div>
|
<div className="text-red-700">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主要内容 */}
|
{/* 主要内容 */}
|
||||||
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-md px-5 py-4 shadow-sm">
|
<div className="bg-[var(--color-message-assistant-bg)] border border-[var(--color-message-assistant-border)] rounded-md px-5 py-4 shadow-sm">
|
||||||
<div className="text-sm text-[var(--color-text-primary)] leading-[1.75]">
|
<div className="text-[var(--color-text-primary)] leading-[1.75]">
|
||||||
{message.content ? (
|
{message.content ? (
|
||||||
<MarkdownRenderer content={message.content} />
|
<MarkdownRenderer content={message.content} />
|
||||||
) : isStreaming ? (
|
) : isStreaming ? (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||||
import { Copy, Check, Eye } from 'lucide-react';
|
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import Prism from 'prismjs';
|
import Prism from 'prismjs';
|
||||||
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
|
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
|
||||||
@ -30,6 +30,10 @@ interface CodeBlockProps {
|
|||||||
language?: string;
|
language?: string;
|
||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** 折叠阈值,超过此行数自动折叠,默认 60 */
|
||||||
|
maxCollapsedLines?: number;
|
||||||
|
/** 折叠时预览的行数,默认 30 */
|
||||||
|
previewLines?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语言别名映射
|
// 语言别名映射
|
||||||
@ -51,9 +55,12 @@ export function CodeBlock({
|
|||||||
language = 'text',
|
language = 'text',
|
||||||
showLineNumbers = true,
|
showLineNumbers = true,
|
||||||
className,
|
className,
|
||||||
|
maxCollapsedLines = 60,
|
||||||
|
previewLines = 30,
|
||||||
}: CodeBlockProps) {
|
}: CodeBlockProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
// 规范化语言名称
|
// 规范化语言名称
|
||||||
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
|
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
|
||||||
@ -62,17 +69,33 @@ export function CodeBlock({
|
|||||||
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
||||||
['html', 'htm'].includes(language.toLowerCase());
|
['html', 'htm'].includes(language.toLowerCase());
|
||||||
|
|
||||||
|
const lines = code.split('\n');
|
||||||
|
const totalLines = lines.length;
|
||||||
|
const shouldCollapse = totalLines > maxCollapsedLines;
|
||||||
|
const remainingLines = totalLines - previewLines;
|
||||||
|
|
||||||
|
// 根据折叠状态获取显示的代码
|
||||||
|
const displayCode = useMemo(() => {
|
||||||
|
if (shouldCollapse && !isExpanded) {
|
||||||
|
return lines.slice(0, previewLines).join('\n');
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}, [code, lines, shouldCollapse, isExpanded, previewLines]);
|
||||||
|
|
||||||
|
const displayLines = displayCode.split('\n');
|
||||||
|
|
||||||
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
||||||
const highlightedCode = useMemo(() => {
|
const highlightedCode = useMemo(() => {
|
||||||
const grammar = Prism.languages[normalizedLanguage];
|
const grammar = Prism.languages[normalizedLanguage];
|
||||||
if (grammar) {
|
if (grammar) {
|
||||||
return Prism.highlight(code, grammar, normalizedLanguage);
|
return Prism.highlight(displayCode, grammar, normalizedLanguage);
|
||||||
}
|
}
|
||||||
return code;
|
return displayCode;
|
||||||
}, [code, normalizedLanguage]);
|
}, [displayCode, normalizedLanguage]);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 始终复制完整代码
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
@ -81,13 +104,15 @@ export function CodeBlock({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const lines = code.split('\n');
|
const toggleExpand = () => {
|
||||||
|
setIsExpanded(!isExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
||||||
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
||||||
{/* 顶部工具栏 */}
|
{/* 顶部工具栏 */}
|
||||||
<div className="flex items-center justify-between px-4 py-2.5 text-sm"
|
<div className="flex items-center justify-between px-4 py-2.5 text-[0.9em]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-code-toolbar-bg)',
|
backgroundColor: 'var(--color-code-toolbar-bg)',
|
||||||
color: 'var(--color-code-toolbar-text)',
|
color: 'var(--color-code-toolbar-text)',
|
||||||
@ -106,6 +131,12 @@ export function CodeBlock({
|
|||||||
style={{ backgroundColor: '#27c93f', boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.1) inset' }} />
|
style={{ backgroundColor: '#27c93f', boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.1) inset' }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-mono text-[13px]">{language || 'code'}</span>
|
<span className="font-mono text-[13px]">{language || 'code'}</span>
|
||||||
|
{/* 显示总行数 */}
|
||||||
|
{shouldCollapse && (
|
||||||
|
<span className="text-[12px] opacity-60">
|
||||||
|
({totalLines} 行)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:操作按钮 */}
|
{/* 右侧:操作按钮 */}
|
||||||
@ -145,42 +176,95 @@ export function CodeBlock({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 代码区域 */}
|
{/* 代码区域 */}
|
||||||
<div className="relative overflow-x-auto"
|
<div style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
||||||
style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
{/* 代码内容 - 使用 flex 布局 */}
|
||||||
|
<div className="flex overflow-x-auto">
|
||||||
|
{/* 行号列 */}
|
||||||
{showLineNumbers && (
|
{showLineNumbers && (
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-12 select-none"
|
<div
|
||||||
|
className="flex-shrink-0 w-12 select-none py-4 px-2 text-right"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-code-bg)',
|
backgroundColor: 'var(--color-code-bg)',
|
||||||
borderRight: '1px solid var(--color-code-line-border)'
|
borderRight: '1px solid var(--color-code-line-border)'
|
||||||
}}>
|
}}
|
||||||
<div className="py-4 px-2 text-right">
|
>
|
||||||
{lines.map((_, index) => (
|
{displayLines.map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="text-sm font-mono leading-6"
|
className="font-mono leading-6"
|
||||||
style={{ color: 'var(--color-code-line-number)' }}
|
style={{ color: 'var(--color-code-line-number)' }}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{/* 代码列 */}
|
||||||
<pre
|
<pre
|
||||||
className={cn(
|
className="flex-1 p-4 overflow-x-auto"
|
||||||
'p-4 overflow-x-auto',
|
|
||||||
showLineNumbers && 'pl-16'
|
|
||||||
)}
|
|
||||||
style={{ margin: 0, background: 'transparent' }}
|
style={{ margin: 0, background: 'transparent' }}
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
className={`language-${normalizedLanguage} text-sm leading-6`}
|
className={`language-${normalizedLanguage} leading-6`}
|
||||||
style={{ color: 'var(--color-code-text)' }}
|
style={{ color: 'var(--color-code-text)' }}
|
||||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||||
/>
|
/>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 渐变遮罩和展开按钮(仅在折叠状态下显示) */}
|
||||||
|
{shouldCollapse && !isExpanded && (
|
||||||
|
<>
|
||||||
|
{/* 渐变遮罩 */}
|
||||||
|
<div
|
||||||
|
className="h-16 -mt-16 pointer-events-none relative z-10"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to bottom, transparent, var(--color-code-bg))`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 展开按钮 */}
|
||||||
|
<div
|
||||||
|
className="py-3 text-center cursor-pointer transition-colors hover:opacity-80"
|
||||||
|
style={{ backgroundColor: 'var(--color-code-bg)' }}
|
||||||
|
onClick={toggleExpand}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
backgroundColor: 'var(--color-primary-alpha)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
<span>展开剩余 {remainingLines} 行代码</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 收起按钮(仅在展开状态下显示) */}
|
||||||
|
{shouldCollapse && isExpanded && (
|
||||||
|
<div
|
||||||
|
className="py-3 text-center cursor-pointer transition-colors"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-code-bg)',
|
||||||
|
borderTop: '1px solid var(--color-code-line-border)'
|
||||||
|
}}
|
||||||
|
onClick={toggleExpand}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-code-toolbar-text)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronUp size={16} />
|
||||||
|
<span>收起代码</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* HTML 预览模态框 */}
|
{/* HTML 预览模态框 */}
|
||||||
{isHtmlPreviewable && (
|
{isHtmlPreviewable && (
|
||||||
<HtmlPreviewModal
|
<HtmlPreviewModal
|
||||||
|
|||||||
@ -48,31 +48,31 @@ const markdownComponents = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 标题
|
// 标题 - 使用相对单位保持与全局字体的比例
|
||||||
h1({ children }: { children?: React.ReactNode }) {
|
h1({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<h1 className="text-lg font-bold mt-5 mb-3 text-[var(--color-text-primary)]">
|
<h1 className="text-[1.25em] font-bold mt-5 mb-3 text-[var(--color-text-primary)]">
|
||||||
{children}
|
{children}
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
h2({ children }: { children?: React.ReactNode }) {
|
h2({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<h2 className="text-base font-bold mt-4 mb-2 text-[var(--color-text-primary)]">
|
<h2 className="text-[1.125em] font-bold mt-4 mb-2 text-[var(--color-text-primary)]">
|
||||||
{children}
|
{children}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
h3({ children }: { children?: React.ReactNode }) {
|
h3({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<h3 className="text-sm font-semibold mt-3 mb-2 text-[var(--color-text-primary)]">
|
<h3 className="text-[1em] font-semibold mt-3 mb-2 text-[var(--color-text-primary)]">
|
||||||
{children}
|
{children}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
h4({ children }: { children?: React.ReactNode }) {
|
h4({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<h4 className="text-sm font-semibold mt-2 mb-1 text-[var(--color-text-primary)]">
|
<h4 className="text-[1em] font-semibold mt-2 mb-1 text-[var(--color-text-primary)]">
|
||||||
{children}
|
{children}
|
||||||
</h4>
|
</h4>
|
||||||
);
|
);
|
||||||
@ -95,7 +95,7 @@ const markdownComponents = {
|
|||||||
},
|
},
|
||||||
li({ children }: { children?: React.ReactNode }) {
|
li({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<li className="leading-relaxed text-sm">
|
<li className="leading-relaxed">
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@ -136,7 +136,7 @@ const markdownComponents = {
|
|||||||
// 引用
|
// 引用
|
||||||
blockquote({ children }: { children?: React.ReactNode }) {
|
blockquote({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<blockquote className="border-l-3 border-[var(--color-primary)] pl-3 my-3 italic text-sm text-[var(--color-text-secondary)]">
|
<blockquote className="border-l-3 border-[var(--color-primary)] pl-3 my-3 italic text-[var(--color-text-secondary)]">
|
||||||
{children}
|
{children}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
@ -153,7 +153,7 @@ const markdownComponents = {
|
|||||||
table({ children }: { children?: React.ReactNode }) {
|
table({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto my-3">
|
<div className="overflow-x-auto my-3">
|
||||||
<table className="min-w-full border border-[var(--color-border)] rounded-lg overflow-hidden text-sm">
|
<table className="min-w-full border border-[var(--color-border)] rounded-lg overflow-hidden text-[0.9em]">
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -182,14 +182,14 @@ const markdownComponents = {
|
|||||||
},
|
},
|
||||||
th({ children }: { children?: React.ReactNode }) {
|
th({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<th className="px-3 py-1.5 text-left text-xs font-semibold text-[var(--color-text-primary)]">
|
<th className="px-3 py-1.5 text-left text-[0.85em] font-semibold text-[var(--color-text-primary)]">
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
td({ children }: { children?: React.ReactNode }) {
|
td({ children }: { children?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<td className="px-3 py-1.5 text-xs text-[var(--color-text-secondary)]">
|
<td className="px-3 py-1.5 text-[0.85em] text-[var(--color-text-secondary)]">
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
Monitor,
|
Monitor,
|
||||||
@ -83,6 +83,29 @@ export function HtmlPreviewModal({
|
|||||||
// 获取当前设备配置
|
// 获取当前设备配置
|
||||||
const currentDevice = deviceConfigs.find((d) => d.type === device) || deviceConfigs[0];
|
const currentDevice = deviceConfigs.find((d) => d.type === device) || deviceConfigs[0];
|
||||||
|
|
||||||
|
// 处理 HTML 代码,注入 <base target="_blank"> 防止链接影响父窗口
|
||||||
|
const processedHtmlCode = useMemo(() => {
|
||||||
|
const baseTag = '<base target="_blank">';
|
||||||
|
|
||||||
|
// 检查是否已经有 <base> 标签
|
||||||
|
if (/<base\s/i.test(htmlCode)) {
|
||||||
|
return htmlCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试在 <head> 标签后注入
|
||||||
|
if (/<head[^>]*>/i.test(htmlCode)) {
|
||||||
|
return htmlCode.replace(/<head[^>]*>/i, `$&\n ${baseTag}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试在 <html> 标签后注入
|
||||||
|
if (/<html[^>]*>/i.test(htmlCode)) {
|
||||||
|
return htmlCode.replace(/<html[^>]*>/i, `$&\n <head>\n ${baseTag}\n </head>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果都没有,在最前面添加
|
||||||
|
return `<head>${baseTag}</head>\n${htmlCode}`;
|
||||||
|
}, [htmlCode]);
|
||||||
|
|
||||||
// ESC 关闭 / F11 全屏切换
|
// ESC 关闭 / F11 全屏切换
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
@ -348,7 +371,7 @@ export function HtmlPreviewModal({
|
|||||||
{/* iframe 预览 */}
|
{/* iframe 预览 */}
|
||||||
<iframe
|
<iframe
|
||||||
key={iframeKey}
|
key={iframeKey}
|
||||||
srcDoc={htmlCode}
|
srcDoc={processedHtmlCode}
|
||||||
sandbox="allow-scripts allow-same-origin"
|
sandbox="allow-scripts allow-same-origin"
|
||||||
className="w-full bg-white"
|
className="w-full bg-white"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user