Версия 1.0.6 Исправлена ошибка, когда при выходе из редактирования статьи не сохранялись состояния фильтров и пагинации.

This commit is contained in:
anibilag 2025-10-19 23:53:31 +03:00
parent 758e9fe821
commit 103705c59a
2 changed files with 219 additions and 178 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "vite-react-typescript-starter", "name": "vite-react-typescript-starter",
"private": true, "private": true,
"version": "1.0.5", "version": "1.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -6,7 +6,7 @@ import { Pencil, Trash2, ImagePlus, ToggleLeft, ToggleRight, Plus } from 'lucide
import MinutesWord from './Words/MinutesWord'; import MinutesWord from './Words/MinutesWord';
import { useAuthStore } from '../stores/authStore'; import { useAuthStore } from '../stores/authStore';
import { usePermissions } from '../hooks/usePermissions'; import { usePermissions } from '../hooks/usePermissions';
import {Pagination} from "./Pagination.tsx"; import { Pagination } from "./Pagination";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
interface ArticleListProps { interface ArticleListProps {
@ -32,15 +32,16 @@ export const ArticleList = React.memo(function ArticleList({
}: ArticleListProps) { }: ArticleListProps) {
const { user } = useAuthStore(); const { user } = useAuthStore();
const { availableCategoryIds, availableCityIds, isAdmin } = usePermissions(); const { availableCategoryIds, availableCityIds, isAdmin } = usePermissions();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
// Получаем значения фильтров из URL
const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const filterCategoryId = parseInt(searchParams.get('categoryId') || '0', 10);
const filterCityId = parseInt(searchParams.get('cityId') || '0', 10);
const showDraftOnly = searchParams.get('draftOnly') === 'true';
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalArticles, setTotalArticles] = useState(0); const [totalArticles, setTotalArticles] = useState(0);
const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const [filterCategoryId, setFilterCategoryId] = useState(0);
const [filterCityId, setFilterCityId] = useState(0);
const [showDraftOnly, setShowDraftOnly] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -49,7 +50,14 @@ export const ArticleList = React.memo(function ArticleList({
setLoading(true); setLoading(true);
try { try {
const response = await axios.get('/api/articles/', { const response = await axios.get('/api/articles/', {
params: { page: currentPage, categoryId: filterCategoryId, cityId: filterCityId, isDraft: showDraftOnly, userId: user?.id, isAdmin }, params: {
page: currentPage,
categoryId: filterCategoryId,
cityId: filterCityId,
isDraft: showDraftOnly,
userId: user?.id,
isAdmin
},
}); });
setArticles(response.data.articles); setArticles(response.data.articles);
setTotalPages(response.data.totalPages); setTotalPages(response.data.totalPages);
@ -74,9 +82,17 @@ export const ArticleList = React.memo(function ArticleList({
{ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } } { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }
); );
setArticles(prev => prev.map(a => (a.id === id ? { ...a, isActive: !a.isActive } : a))); setArticles(prev => prev.map(a => (a.id === id ? { ...a, isActive: !a.isActive } : a)));
// Обновляем список с сервера после изменения статуса // Обновляем список с сервера после изменения статуса
const response = await axios.get('/api/articles/', { const response = await axios.get('/api/articles/', {
params: { page: currentPage, categoryId: filterCategoryId, cityId: filterCityId, isDraft: showDraftOnly, userId: user?.id, isAdmin }, params: {
page: currentPage,
categoryId: filterCategoryId,
cityId: filterCityId,
isDraft: showDraftOnly,
userId: user?.id,
isAdmin
},
}); });
setArticles(response.data.articles); setArticles(response.data.articles);
setTotalPages(response.data.totalPages); setTotalPages(response.data.totalPages);
@ -88,11 +104,46 @@ export const ArticleList = React.memo(function ArticleList({
}; };
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
searchParams.set('page', page.toString()); const newParams = new URLSearchParams(searchParams);
setSearchParams(searchParams); newParams.set('page', page.toString());
setSearchParams(newParams);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}; };
// Обработчики изменения фильтров
const handleCategoryChange = (categoryId: number) => {
const newParams = new URLSearchParams(searchParams);
if (categoryId === 0) {
newParams.delete('categoryId');
} else {
newParams.set('categoryId', categoryId.toString());
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleCityChange = (cityId: number) => {
const newParams = new URLSearchParams(searchParams);
if (cityId === 0) {
newParams.delete('cityId');
} else {
newParams.set('cityId', cityId.toString());
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleDraftOnlyChange = (checked: boolean) => {
const newParams = new URLSearchParams(searchParams);
if (checked) {
newParams.set('draftOnly', 'true');
} else {
newParams.delete('draftOnly');
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const hasNoPermissions = availableCategoryIds.length === 0 || availableCityIds.length === 0; const hasNoPermissions = availableCategoryIds.length === 0 || availableCityIds.length === 0;
return ( return (
@ -103,67 +154,58 @@ export const ArticleList = React.memo(function ArticleList({
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
<select <select
value={filterCategoryId} value={filterCategoryId}
onChange={(e) => { onChange={(e) => handleCategoryChange(Number(e.target.value))}
setFilterCategoryId(Number(e.target.value));
searchParams.set('page', '1');
setSearchParams(searchParams);
}}
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm" className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
> >
<option value="">Все категории</option> <option value="0">Все категории</option>
{availableCategoryIds.map(cat => ( {availableCategoryIds.map(cat => (
<option key={cat} value={cat}> <option key={cat} value={cat}>
{CategoryTitles[cat]} {CategoryTitles[cat]}
</option> </option>
))} ))}
</select> </select>
<select <select
value={filterCityId} value={filterCityId}
onChange={(e) => { onChange={(e) => handleCityChange(Number(e.target.value))}
setFilterCityId(Number(e.target.value));
searchParams.set('page', '1');
setSearchParams(searchParams);
}}
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm" className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
> >
<option value="">Все столицы</option> <option value="0">Все столицы</option>
{availableCityIds.map(c => ( {availableCityIds.map(c => (
<option key={c} value={c}> <option key={c} value={c}>
{CityTitles[c]} {CityTitles[c]}
</option> </option>
))} ))}
</select> </select>
<label className="inline-flex items-center"> <label className="inline-flex items-center">
<input <input
type="checkbox" type="checkbox"
checked={showDraftOnly} checked={showDraftOnly}
onChange={(e) => { onChange={(e) => handleDraftOnlyChange(e.target.checked)}
setShowDraftOnly(e.target.checked)
searchParams.set('page', '1');
setSearchParams(searchParams);
}}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/> />
<span className="ml-2 text-sm text-gray-700">Только черновики</span> <span className="ml-2 text-sm text-gray-700">Только черновики</span>
</label> </label>
{!hasNoPermissions && ( {!hasNoPermissions && (
<button <button
onClick={onNewArticle} onClick={onNewArticle}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50" className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
> >
<Plus size={16} className="mr-2" /> Новая статья <Plus size={16} className="mr-2" />
Новая статья
</button> </button>
)} )}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
<p className="font-bold text-gray-600"> <p className="font-bold text-gray-600">
Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)} Статьи {Math.min((currentPage - 1) * ARTICLES_PER_PAGE + 1, totalArticles)} - {Math.min(currentPage * ARTICLES_PER_PAGE, totalArticles)} из {totalArticles}
-
{Math.min(currentPage * ARTICLES_PER_PAGE, totalArticles)} из {totalArticles}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{loading ? ( {loading ? (
<div className="flex justify-center p-6"> <div className="flex justify-center p-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
@ -190,10 +232,8 @@ export const ArticleList = React.memo(function ArticleList({
.filter(a => a.role === 'WRITER') .filter(a => a.role === 'WRITER')
.sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0)); .sort((a, b) => (a.author.order ?? 0) - (b.author.order ?? 0));
if (!writerAuthors) return null; if (!writerAuthors) return null;
return ( return (
<div className="flex items-center text-xs text-gray-500 mt-1"> <div className="flex items-center text-xs text-gray-500 mt-1">
{writerAuthors.map((authorLink) => ( {writerAuthors.map((authorLink) => (
<img <img
key={authorLink.author.id} key={authorLink.author.id}
@ -202,7 +242,6 @@ export const ArticleList = React.memo(function ArticleList({
className="h-6 w-6 rounded-full mr-1" className="h-6 w-6 rounded-full mr-1"
/> />
))} ))}
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900">
{writerAuthors.map((a, i) => ( {writerAuthors.map((a, i) => (
@ -248,6 +287,7 @@ export const ArticleList = React.memo(function ArticleList({
</li> </li>
))} ))}
</ul> </ul>
{totalPages > 1 && ( {totalPages > 1 && (
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}
@ -258,6 +298,7 @@ export const ArticleList = React.memo(function ArticleList({
<span className="ml-2 text-sm text-gray-700"></span> <span className="ml-2 text-sm text-gray-700"></span>
</> </>
)} )}
{error && isAdmin && ( {error && isAdmin && (
<div className="p-4 text-red-700 bg-red-50 rounded-md">{error}</div> <div className="p-4 text-red-700 bg-red-50 rounded-md">{error}</div>
)} )}