Files
stu-ai-demo/src/components/data-details/detail-code-block.tsx

184 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import * as React from 'react'
import { Copy, Check, Maximize2 } from 'lucide-react'
import copy from 'copy-to-clipboard'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import { Highlight, themes } from 'prism-react-renderer'
import { useTheme } from 'next-themes'
export interface DetailCodeBlockProps {
code: string
language?: string
title?: string
copyable?: boolean
showLineNumbers?: boolean
maxHeight?: string
className?: string
}
/**
* 代码块组件
* 展示代码或JSON数据支持复制和行号显示功能
*/
export function DetailCodeBlock({
code,
language = 'text',
title,
copyable = true,
showLineNumbers = true,
maxHeight = '400px',
className,
}: DetailCodeBlockProps) {
const [copied, setCopied] = React.useState(false)
const [fullscreenOpen, setFullscreenOpen] = React.useState(false)
const { theme } = useTheme()
const handleCopy = () => {
const success = copy(code)
if (success) {
setCopied(true)
toast.success('已复制到剪贴板')
setTimeout(() => setCopied(false), 2000)
} else {
toast.error('复制失败')
}
}
// 根据主题选择合适的代码高亮主题
const prismTheme = theme === 'dark' ? themes.vsDark : themes.vsLight
// 渲染代码高亮内容
const renderCodeContent = (isFullscreen = false) => (
<Highlight
theme={prismTheme}
code={code}
language={language as any}
>
{({ className: highlightClassName, style, tokens, getLineProps, getTokenProps }) => {
// 提取背景色用于外层容器
const backgroundColor = style?.backgroundColor
// 计算行号的最大宽度
const lineNumberWidth = String(tokens.length).length
return (
<div
className="overflow-auto p-4"
style={{
maxHeight: isFullscreen ? undefined : maxHeight,
backgroundColor
}}
>
<pre
className={cn(
'text-sm leading-relaxed',
isFullscreen && 'whitespace-pre',
highlightClassName
)}
style={{ ...style, backgroundColor: 'transparent' }}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line })} className="table-row">
{showLineNumbers && (
<span
className="table-cell select-none pr-4 text-right opacity-50"
style={{
width: `${lineNumberWidth + 1}ch`,
minWidth: `${lineNumberWidth + 1}ch`,
}}
>
{i + 1}
</span>
)}
<span className="table-cell">
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</div>
))}
</pre>
</div>
)
}}
</Highlight>
)
return (
<>
<div className={cn('relative rounded-lg border bg-muted/50', className)}>
{(title || copyable) && (
<div className="flex items-center justify-between gap-3 border-b px-4 py-3 bg-muted/30">
{title && (
<div className="text-sm font-semibold flex-1 break-words min-w-0 self-center">{title}</div>
)}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-8 px-3"
onClick={() => setFullscreenOpen(true)}
>
<Maximize2 className="h-3.5 w-3.5 mr-1.5" />
</Button>
{copyable && (
<Button
variant="ghost"
size="sm"
className="h-8 px-3"
onClick={handleCopy}
>
{copied ? (
<>
<Check className="h-3.5 w-3.5 mr-1.5" />
</>
) : (
<>
<Copy className="h-3.5 w-3.5 mr-1.5" />
</>
)}
</Button>
)}
</div>
</div>
)}
{renderCodeContent()}
</div>
{/* 全屏对话框 */}
<Dialog open={fullscreenOpen} onOpenChange={setFullscreenOpen}>
<DialogContent className="p-0" variant="fullscreen">
<DialogHeader className="pt-5 pb-3 m-0 border-b border-border">
<DialogTitle className="px-6 text-base">{title || '代码查看'}</DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
<DialogBody className="overflow-auto">
{renderCodeContent(true)}
</DialogBody>
<DialogFooter className="px-6 py-4 border-t border-border">
<DialogClose asChild>
<Button type="button"></Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}