Hair Keeper v1.0.0:一个高度集成、深度定制、约定优于配置的全栈Web应用模板,旨在保持灵活性的同时提供一套基于成熟架构的开发底座,自带身份认证、权限控制、丰富前端组件、文件上传、后台任务、智能体开发等丰富功能,提供AI开发辅助,免于纠结功能如何实现,可快速上手专注于业务逻辑
This commit is contained in:
105
src/components/data-details/detail-list.tsx
Normal file
105
src/components/data-details/detail-list.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Search, type LucideIcon } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface DetailListProps {
|
||||
items: Array<{
|
||||
id: string
|
||||
label: string
|
||||
description?: string
|
||||
icon?: LucideIcon
|
||||
onClick?: () => void
|
||||
}>
|
||||
searchable?: boolean
|
||||
emptyText?: string
|
||||
maxHeight?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用列表组件
|
||||
* 展示项目列表,支持图标、描述、操作和搜索
|
||||
*/
|
||||
export function DetailList({
|
||||
items,
|
||||
searchable = false,
|
||||
emptyText = '暂无数据',
|
||||
maxHeight = '300px',
|
||||
className,
|
||||
}: DetailListProps) {
|
||||
const [searchQuery, setSearchQuery] = React.useState('')
|
||||
|
||||
const filteredItems = React.useMemo(() => {
|
||||
if (!searchQuery) return items
|
||||
const query = searchQuery.toLowerCase()
|
||||
return items.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(query) ||
|
||||
item.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}, [items, searchQuery])
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className={cn('text-sm text-muted-foreground text-center py-4', className)}>
|
||||
{emptyText}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{searchable && (
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 h-9"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="space-y-1.5 overflow-y-auto rounded-md border bg-muted/30 p-2"
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground text-center py-8">
|
||||
未找到匹配项
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'flex items-start gap-3 rounded-md p-3 text-sm transition-colors',
|
||||
item.onClick &&
|
||||
'cursor-pointer hover:bg-accent hover:text-accent-foreground'
|
||||
)}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium break-words leading-relaxed">{item.label}</div>
|
||||
{item.description && (
|
||||
<div className="text-xs text-muted-foreground mt-1 break-words leading-relaxed">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user