feat: Улучшение компонентов Dashboard, EmptyState, ErrorAlert и Pagination. Добавлены новые функции, такие как индикаторы тренда, возможность перехода на страницу и улучшенные действия с ошибками. Обновлены стили для повышения удобства использования и доступности.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { useState } from 'react'
|
||||
import { IconChevronRight, IconDotsVertical } from '@tabler/icons-react'
|
||||
import ContextMenu from './ContextMenu.jsx'
|
||||
|
||||
/**
|
||||
* MobileCardView - компонент для отображения табличных данных в виде карточек на мобильных
|
||||
* Поддерживает swipe gestures для действий
|
||||
*/
|
||||
function MobileCardView({
|
||||
items,
|
||||
onItemClick,
|
||||
renderContent, // функция для рендера содержимого карточки
|
||||
actions // массив действий { label, icon, onClick, variant }
|
||||
}) {
|
||||
const [swipedItem, setSwipedItem] = useState(null)
|
||||
const [touchStart, setTouchStart] = useState(null)
|
||||
const [touchEnd, setTouchEnd] = useState(null)
|
||||
|
||||
// минимальная дистанция свайпа в px
|
||||
const minSwipeDistance = 50
|
||||
|
||||
const onTouchStart = (e, item) => {
|
||||
setTouchEnd(null)
|
||||
setTouchStart(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const onTouchMove = (e, item) => {
|
||||
setTouchEnd(e.targetTouches[0].clientX)
|
||||
}
|
||||
|
||||
const onTouchEnd = (item) => {
|
||||
if (!touchStart || !touchEnd) return
|
||||
|
||||
const distance = touchStart - touchEnd
|
||||
const isLeftSwipe = distance > minSwipeDistance
|
||||
const isRightSwipe = distance < -minSwipeDistance
|
||||
|
||||
if (isLeftSwipe) {
|
||||
setSwipedItem(item)
|
||||
} else if (isRightSwipe) {
|
||||
setSwipedItem(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-card-view d-md-none">
|
||||
{items.map((item, index) => {
|
||||
const isSwiped = swipedItem === item
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id || index}
|
||||
className={`mobile-card ${isSwiped ? 'swiped' : ''}`}
|
||||
onTouchStart={(e) => onTouchStart(e, item)}
|
||||
onTouchMove={(e) => onTouchMove(e, item)}
|
||||
onTouchEnd={() => onTouchEnd(item)}
|
||||
onClick={() => !isSwiped && onItemClick && onItemClick(item)}
|
||||
>
|
||||
<div className="mobile-card-content">
|
||||
{renderContent(item)}
|
||||
|
||||
<div className="mobile-card-arrow">
|
||||
<IconChevronRight size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Меню действий */}
|
||||
<div className="mobile-card-actions">
|
||||
<ContextMenu items={actions.map(action => ({
|
||||
...action,
|
||||
onClick: () => action.onClick(item)
|
||||
}))}>
|
||||
<button className="btn btn-icon btn-ghost-secondary">
|
||||
<IconDotsVertical size={20} />
|
||||
</button>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
|
||||
{/* Swipe actions */}
|
||||
{isSwiped && (
|
||||
<div className="mobile-card-swipe-actions">
|
||||
{actions.slice(0, 2).map((action, idx) => {
|
||||
const Icon = action.icon
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
className={`btn btn-${action.variant || 'primary'} btn-icon`}
|
||||
onClick={() => action.onClick(item)}
|
||||
title={action.label}
|
||||
>
|
||||
{Icon && <Icon size={20} />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<style jsx>{`
|
||||
.mobile-card-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-card {
|
||||
position: relative;
|
||||
background: var(--tblr-bg-surface, white);
|
||||
border: 1px solid var(--tblr-border-color, #e9ecef);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-card.swiped .mobile-card-content {
|
||||
transform: translateX(-80px);
|
||||
}
|
||||
|
||||
.mobile-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
transition: transform 0.3s ease;
|
||||
background: var(--tblr-bg-surface, white);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mobile-card-content > :first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile-card-arrow {
|
||||
color: var(--tblr-muted, #6c757d);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-card-actions {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.mobile-card-swipe-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mobile-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-card-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileCardView
|
||||
|
||||
Reference in New Issue
Block a user