Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑

This commit is contained in:
2025-11-13 15:24:54 +08:00
commit 42be39b343
249 changed files with 38843 additions and 0 deletions

View 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>
</>
)
}