feat: Добавление функциональности массового выбора и действий в компоненты ASNsNewManager, DomainsNewManager и IPRangesManager. Реализованы функции для выбора всех элементов, удаления и экспорта выбранных записей, а также добавлены новые иконки и улучшены стили для повышения удобства использования.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m45s

This commit is contained in:
2025-11-12 10:40:31 +07:00
parent ae88a3a46a
commit 2160e58166
4 changed files with 410 additions and 312 deletions
+76 -127
View File
@@ -23,7 +23,8 @@ import {
IconTrendingUp,
IconUsers,
IconArrowUp,
IconArrowDown
IconArrowDown,
IconCopy
} from '@tabler/icons-react';
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
import PageHeader from './components/PageHeader.jsx';
@@ -44,6 +45,7 @@ import AccordionCard from './components/AccordionCard.jsx';
import SavedFilters from './components/SavedFilters.jsx';
import BulkActionsBar from './components/BulkActionsBar.jsx';
import FormField from './components/FormField.jsx';
import Tooltip from './components/Tooltip.jsx';
import { useToast } from './components/ToastContainer.jsx';
const API_URL = '/api';
@@ -171,6 +173,19 @@ function DomainsNewManager() {
const handleDeleteItem = (domainNameToDelete) => {
setItems(items.filter(i => i.domain !== domainNameToDelete));
setSelectedDomains(prev => {
const next = new Set(prev);
next.delete(domainNameToDelete);
return next;
});
};
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
toast.success('Скопировано в буфер обмена');
}).catch(() => {
toast.error('Не удалось скопировать');
});
};
const confirmDelete = (item) => {
@@ -448,10 +463,11 @@ function DomainsNewManager() {
const handleBulkDelete = () => {
if (selectedDomains.size === 0) return;
const count = selectedDomains.size;
const newItems = items.filter(i => !selectedDomains.has(i.domain));
setItems(newItems);
setSelectedDomains(new Set());
toast.success(`Удалено ${selectedDomains.size} доменов`);
toast.success(`Удалено ${count} доменов`);
};
const handleBulkExport = () => {
@@ -536,96 +552,6 @@ function DomainsNewManager() {
)}
/>
{/* Статистические карточки */}
<div className="row g-3 mb-4">
<div className="col-sm-6 col-lg-3">
<div className="card card-sm animate-in">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-blue text-white avatar">
<IconWorld size={24} />
</span>
</div>
<div className="col">
<div className="font-weight-medium">Всего доменов</div>
<div className="text-muted">
<span className="h2 mb-0">{items.length}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-sm-6 col-lg-3">
<div className="card card-sm animate-in" style={{animationDelay: '0.1s'}}>
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-green text-white avatar">
<IconFilter size={24} />
</span>
</div>
<div className="col">
<div className="font-weight-medium">Отфильтровано</div>
<div className="text-muted">
<span className="h2 mb-0">{filtered.length}</span>
{filtered.length !== items.length && (
<span className="text-muted small ms-1">
({items.length - filtered.length} скрыто)
</span>
)}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-sm-6 col-lg-3">
<div className="card card-sm animate-in" style={{animationDelay: '0.2s'}}>
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-purple text-white avatar">
<IconUsers size={24} />
</span>
</div>
<div className="col">
<div className="font-weight-medium">Уникальных Community</div>
<div className="text-muted">
<span className="h2 mb-0">{allCommunities.length}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-sm-6 col-lg-3">
<div className="card card-sm animate-in" style={{animationDelay: '0.3s'}}>
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-orange text-white avatar">
<IconTrendingUp size={24} />
</span>
</div>
<div className="col">
<div className="font-weight-medium">Выбрано</div>
<div className="text-muted">
<span className="h2 mb-0">{selectedDomains.size}</span>
{selectedDomains.size > 0 && (
<span className="text-muted small ms-1">
({Math.round((selectedDomains.size / filtered.length) * 100)}%)
</span>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="row">
<div className="col-lg-3">
{/* Classic Add New Domain Card (improved) */}
@@ -908,14 +834,14 @@ function DomainsNewManager() {
)}
</div>
</th>
<th className="text-end"> </th>
<th className="text-end">Действия</th>
</tr>
</thead>
<tbody>
{paginatedItems.map((item, idx) => (
<tr
key={item.domain}
className={editingDomain === item.domain ? 'table-active' : ''}
className={editingDomain === item.domain ? 'table-active' : (selectedDomains.has(item.domain) ? 'table-selected' : '')}
style={{
animation: `fadeIn 0.3s ease ${idx * 0.02}s both`
}}
@@ -927,12 +853,22 @@ function DomainsNewManager() {
checked={selectedDomains.has(item.domain)}
onChange={() => handleToggleSelect(item.domain)}
aria-label={`Выбрать ${item.domain}`}
onClick={(e) => e.stopPropagation()}
/>
</td>
<td>
<div className="d-flex align-items-center">
<IconWorld size={16} className="me-2 text-muted" />
<span className="font-monospace">{item.domain}</span>
<Tooltip content="Копировать домен">
<button
className="btn btn-ghost-secondary btn-icon btn-sm ms-2"
onClick={() => copyToClipboard(item.domain)}
aria-label="Копировать"
>
<IconCopy size={14} />
</button>
</Tooltip>
</div>
</td>
<td>
@@ -952,42 +888,55 @@ function DomainsNewManager() {
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
style={{ width: '150px' }}
/>
<button
className="btn btn-success btn-icon btn-sm"
onClick={() => handleSaveEdit(item.domain)}
disabled={!isValidCommunity(editingValue)}
aria-label="Сохранить"
title="Сохранить (Enter)"
>
<IconCheck size={16} />
</button>
<button
className="btn btn-secondary btn-icon btn-sm"
onClick={handleCancelEdit}
aria-label="Отмена"
title="Отмена (Esc)"
>
<IconX size={16} />
</button>
<Tooltip content="Сохранить (Enter)">
<button
className="btn btn-success btn-icon btn-sm"
onClick={() => handleSaveEdit(item.domain)}
disabled={!isValidCommunity(editingValue)}
aria-label="Сохранить"
>
<IconCheck size={16} />
</button>
</Tooltip>
<Tooltip content="Отмена (Esc)">
<button
className="btn btn-secondary btn-icon btn-sm"
onClick={handleCancelEdit}
aria-label="Отмена"
>
<IconX size={16} />
</button>
</Tooltip>
</>
) : (
<>
<button
className="btn btn-outline-primary btn-icon btn-sm"
aria-label="Редактировать"
onClick={() => handleEdit(item)}
title="Редактировать community"
>
<IconEdit size={16} />
</button>
<button
className="btn btn-outline-danger btn-icon btn-sm"
aria-label="Удалить"
onClick={() => confirmDelete(item)}
title="Удалить домен"
>
<IconTrash size={16} />
</button>
<Tooltip content="Редактировать">
<button
className="btn btn-outline-primary btn-icon btn-sm"
aria-label="Редактировать"
onClick={() => handleEdit(item)}
>
<IconEdit size={16} />
</button>
</Tooltip>
<Tooltip content="Копировать">
<button
className="btn btn-outline-secondary btn-icon btn-sm"
aria-label="Копировать"
onClick={() => copyToClipboard(`${item.domain} ${item.community}`)}
>
<IconCopy size={16} />
</button>
</Tooltip>
<Tooltip content="Удалить">
<button
className="btn btn-outline-danger btn-icon btn-sm"
aria-label="Удалить"
onClick={() => confirmDelete(item)}
>
<IconTrash size={16} />
</button>
</Tooltip>
</>
)}
</div>