Files
hair-keeper/src/components/data-details/detail-list.tsx

105 lines
2.9 KiB
TypeScript

'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>
)
}