forked from admin/hair-keeper
184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
'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>
|
||
</>
|
||
)
|
||
} |