forked from admin/hair-keeper
Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
184
src/components/data-details/detail-code-block.tsx
Normal file
184
src/components/data-details/detail-code-block.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user