v2.9.0: Улучшения экрана списка задач - оптимизация загрузки, toast уведомления, исправление центрирования
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 44s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 44s
This commit is contained in:
@@ -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 ${
|
||||
|
||||
Reference in New Issue
Block a user