перетаскивание эталонов
This commit is contained in:
@@ -9,10 +9,28 @@ import {
|
||||
import { Input } from '@/component/ui/input'
|
||||
import { Separator } from '@/component/ui/separator'
|
||||
import { useToast } from '@/lib/hooks/useToast'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
arrayMove,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
Check,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
GripVertical,
|
||||
Search,
|
||||
Settings,
|
||||
X,
|
||||
@@ -21,6 +39,89 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useStandards } from './hooks'
|
||||
import { Standard } from './types'
|
||||
|
||||
// Компонент для сортируемого элемента эталона
|
||||
interface SortableStandardItemProps {
|
||||
standard: Standard
|
||||
onRemove: (standardId: string) => void
|
||||
isDragActive: boolean
|
||||
}
|
||||
|
||||
function SortableStandardItem({
|
||||
standard,
|
||||
onRemove,
|
||||
isDragActive,
|
||||
}: SortableStandardItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: standard.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: isDragging ? 'none' : transition,
|
||||
opacity: isDragging ? 0.9 : 1,
|
||||
zIndex: isDragging ? 1000 : 'auto',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`flex cursor-grab select-none items-center gap-3 rounded-md border border-primary bg-primary/10 px-3 py-2 shadow-sm ${
|
||||
isDragging
|
||||
? 'scale-105 shadow-lg'
|
||||
: !isDragActive
|
||||
? 'transition-shadow duration-200 hover:shadow-md'
|
||||
: ''
|
||||
} active:cursor-grabbing`}
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center text-muted-foreground">
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="min-w-20 shrink-0 text-sm font-medium text-foreground">
|
||||
{standard.protocol_name}
|
||||
</div>
|
||||
<div className="truncate text-sm text-muted-foreground">
|
||||
{standard.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<FileText className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">{standard.registry_number}</span>
|
||||
</div>
|
||||
<Badge variant="secondary" className="px-1.5 py-0.5 text-xs">
|
||||
{standard.range}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onRemove(standard.id)
|
||||
}}
|
||||
onPointerDown={e => e.stopPropagation()}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface StandardsSelectorProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
@@ -37,12 +138,51 @@ export function StandardsSelector({
|
||||
maxItems = 7, // Значение по умолчанию
|
||||
}: StandardsSelectorProps) {
|
||||
const [selectedStandards, setSelectedStandards] = useState<string[]>(value)
|
||||
const [isDragActive, setIsDragActive] = useState(false)
|
||||
|
||||
// Синхронизируем selectedStandards с value при изменении value
|
||||
useEffect(() => {
|
||||
setSelectedStandards(value)
|
||||
}, [value])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const { error } = useToast()
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { data: standards = [], isLoading, error: apiError } = useStandards()
|
||||
|
||||
// Настройка сенсоров для drag-and-drop
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10, // Начинать перетаскивание после движения на 10px
|
||||
tolerance: 5,
|
||||
delay: 100,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
)
|
||||
|
||||
// Обработчик начала перетаскивания
|
||||
const handleDragStart = () => {
|
||||
setIsDragActive(true)
|
||||
}
|
||||
|
||||
// Обработчик завершения перетаскивания
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setIsDragActive(false)
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setSelectedStandards(items => {
|
||||
const oldIndex = items.indexOf(active.id as string)
|
||||
const newIndex = items.indexOf(over.id as string)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Автофокус на поле поиска при открытии
|
||||
useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
@@ -119,9 +259,10 @@ export function StandardsSelector({
|
||||
)
|
||||
}, [searchTerm, standards])
|
||||
|
||||
const selectedStandardsData = standards.filter(standard =>
|
||||
selectedStandards.includes(standard.id)
|
||||
)
|
||||
// Создаем массив выбранных эталонов в том порядке, как они выбраны
|
||||
const selectedStandardsData = selectedStandards
|
||||
.map(standardId => standards.find(standard => standard.id === standardId))
|
||||
.filter((standard): standard is Standard => standard !== undefined)
|
||||
|
||||
const unselectedStandards = filteredStandards.filter(
|
||||
standard => !selectedStandards.includes(standard.id)
|
||||
@@ -346,11 +487,31 @@ export function StandardsSelector({
|
||||
<div>
|
||||
<h4 className="mb-3 text-sm font-medium text-foreground">
|
||||
Выбранные эталоны ({selectedStandardsData.length})
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
Перетащите для изменения порядка
|
||||
</span>
|
||||
</h4>
|
||||
<div className="max-h-64 space-y-1 overflow-y-auto">
|
||||
{selectedStandardsData.map(standard =>
|
||||
renderStandardCard(standard, true, true)
|
||||
)}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={selectedStandards}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{selectedStandardsData.map(standard => (
|
||||
<SortableStandardItem
|
||||
key={standard.id}
|
||||
standard={standard}
|
||||
onRemove={removeStandard}
|
||||
isDragActive={isDragActive}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user