v2.9.0: Улучшения экрана списка задач - оптимизация загрузки, toast уведомления, исправление центрирования
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 44s

This commit is contained in:
poignatov
2026-01-04 19:37:59 +03:00
parent 6d7d59d2ae
commit 79430ba7f0
8 changed files with 1023 additions and 7 deletions

View File

@@ -8,6 +8,8 @@ import TestConfigSelection from './components/TestConfigSelection'
import AddConfig from './components/AddConfig'
import TestWords from './components/TestWords'
import Profile from './components/Profile'
import TaskList from './components/TaskList'
import TaskForm from './components/TaskForm'
import { AuthProvider, useAuth } from './components/auth/AuthContext'
import AuthScreen from './components/auth/AuthScreen'
@@ -42,6 +44,8 @@ function AppContent() {
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
})
@@ -55,6 +59,8 @@ function AppContent() {
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
})
@@ -64,16 +70,19 @@ function AppContent() {
// Кеширование данных
const [currentWeekData, setCurrentWeekData] = useState(null)
const [fullStatisticsData, setFullStatisticsData] = useState(null)
const [tasksData, setTasksData] = useState(null)
// Состояния загрузки для каждого таба (показываются только при первой загрузке)
const [currentWeekLoading, setCurrentWeekLoading] = useState(false)
const [fullStatisticsLoading, setFullStatisticsLoading] = useState(false)
const [prioritiesLoading, setPrioritiesLoading] = useState(false)
const [tasksLoading, setTasksLoading] = useState(false)
// Состояния фоновой загрузки (не показываются визуально)
const [currentWeekBackgroundLoading, setCurrentWeekBackgroundLoading] = useState(false)
const [fullStatisticsBackgroundLoading, setFullStatisticsBackgroundLoading] = useState(false)
const [prioritiesBackgroundLoading, setPrioritiesBackgroundLoading] = useState(false)
const [tasksBackgroundLoading, setTasksBackgroundLoading] = useState(false)
// Ошибки
const [currentWeekError, setCurrentWeekError] = useState(null)
@@ -94,7 +103,7 @@ function AppContent() {
try {
const savedTab = window.localStorage?.getItem('activeTab')
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'profile']
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'tasks', 'task-form', 'profile']
if (savedTab && validTabs.includes(savedTab)) {
setActiveTab(savedTab)
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
@@ -194,6 +203,30 @@ function AppContent() {
}
}, [authFetch])
const fetchTasksData = useCallback(async (isBackground = false) => {
try {
if (isBackground) {
setTasksBackgroundLoading(true)
} else {
setTasksLoading(true)
}
const response = await authFetch('/api/tasks')
if (!response.ok) {
throw new Error('Ошибка загрузки данных')
}
const jsonData = await response.json()
setTasksData(jsonData)
} catch (err) {
console.error('Ошибка загрузки списка задач:', err)
} finally {
if (isBackground) {
setTasksBackgroundLoading(false)
} else {
setTasksLoading(false)
}
}
}, [authFetch])
// Используем ref для отслеживания инициализации табов (чтобы избежать лишних пересозданий функции)
const tabsInitializedRef = useRef({
current: false,
@@ -204,6 +237,8 @@ function AppContent() {
'test-config': false,
'add-config': false,
test: false,
tasks: false,
'task-form': false,
profile: false,
})
@@ -211,6 +246,7 @@ function AppContent() {
const cacheRef = useRef({
current: null,
full: null,
tasks: null,
})
// Обновляем ref при изменении данных
@@ -222,6 +258,10 @@ function AppContent() {
cacheRef.current.full = fullStatisticsData
}, [fullStatisticsData])
useEffect(() => {
cacheRef.current.tasks = tasksData
}, [tasksData])
// Функция для загрузки данных таба
const loadTabData = useCallback((tab, isBackground = false) => {
if (tab === 'current') {
@@ -275,8 +315,21 @@ function AppContent() {
// Возврат на таб - фоновая загрузка
setTestConfigRefreshTrigger(prev => prev + 1)
}
} else if (tab === 'tasks') {
const hasCache = cacheRef.current.tasks !== null
const isInitialized = tabsInitializedRef.current.tasks
if (!isInitialized) {
// Первая загрузка таба - загружаем с индикатором
fetchTasksData(false)
tabsInitializedRef.current.tasks = true
setTabsInitialized(prev => ({ ...prev, tasks: true }))
} else if (hasCache && isBackground) {
// Возврат на таб с кешем - фоновая загрузка
fetchTasksData(true)
}
}
}, [fetchCurrentWeekData, fetchFullStatisticsData])
}, [fetchCurrentWeekData, fetchFullStatisticsData, fetchTasksData])
// Функция для обновления всех данных (для кнопки Refresh, если она есть)
const refreshAllData = useCallback(async () => {
@@ -325,13 +378,19 @@ function AppContent() {
if (tab === 'full' && activeTab === 'full') {
// При повторном клике на "Полная статистика" сбрасываем выбранный проект
setSelectedProject(null)
} else if (tab !== activeTab) {
} else if (tab !== activeTab || tab === 'task-form') {
// Для task-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab)
// Сбрасываем tabParams при переходе с add-config на другой таб
if (activeTab === 'add-config' && tab !== 'add-config') {
setTabParams({})
} else {
setTabParams(params)
// Для task-form явно удаляем taskId, если он undefined
if (tab === 'task-form' && params.taskId === undefined) {
setTabParams({})
} else {
setTabParams(params)
}
}
setActiveTab(tab)
if (tab === 'current') {
@@ -341,6 +400,11 @@ function AppContent() {
if (activeTab === 'add-words' && tab === 'words') {
setWordsRefreshTrigger(prev => prev + 1)
}
// Обновляем список задач при возврате из экрана редактирования
// Используем фоновую загрузку, чтобы не показывать индикатор загрузки
if (activeTab === 'task-form' && tab === 'tasks') {
fetchTasksData(true)
}
// Загрузка данных произойдет в useEffect при изменении activeTab
}
}
@@ -393,7 +457,7 @@ function AppContent() {
}, [activeTab])
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config'
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'add-config' || activeTab === 'task-form'
return (
<div className="flex flex-col min-h-screen min-h-dvh">
@@ -493,6 +557,28 @@ function AppContent() {
</div>
)}
{loadedTabs.tasks && (
<div className={activeTab === 'tasks' ? 'block' : 'hidden'}>
<TaskList
onNavigate={handleNavigate}
data={tasksData}
loading={tasksLoading}
backgroundLoading={tasksBackgroundLoading}
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
/>
</div>
)}
{loadedTabs['task-form'] && (
<div className={activeTab === 'task-form' ? 'block' : 'hidden'}>
<TaskForm
key={tabParams.taskId || 'new'}
onNavigate={handleNavigate}
taskId={tabParams.taskId}
/>
</div>
)}
{loadedTabs.profile && (
<div className={activeTab === 'profile' ? 'block' : 'hidden'}>
<Profile onNavigate={handleNavigate} />
@@ -546,6 +632,25 @@ function AppContent() {
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('tasks')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
activeTab === 'tasks' || activeTab === 'task-form'
? 'text-indigo-700 bg-white/50'
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
}`}
title="Задачи"
>
<span className="relative z-10 flex items-center justify-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
</span>
{(activeTab === 'tasks' || activeTab === 'task-form') && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
)}
</button>
<button
onClick={() => handleTabChange('profile')}
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${