105 lines
2.9 KiB
TypeScript
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>
|
|
)
|
|
} |