4.13.6: Рефакторинг архитектуры табов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s

This commit is contained in:
poignatov
2026-02-04 14:35:47 +03:00
parent 794947ea89
commit 8023319ee4
3 changed files with 112 additions and 106 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "play-life-web",
"version": "4.13.5",
"version": "4.13.6",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -130,8 +130,6 @@ function AppContent() {
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0)
// Состояние для сохранения позиции скролла каждого таба
const [scrollPositions, setScrollPositions] = useState({})
// Восстанавливаем последний выбранный таб после перезагрузки
@@ -710,15 +708,6 @@ function AppContent() {
setTabParams({})
updateUrl('full', {}, activeTab)
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form' || (tab === 'words' && Object.keys(params).length > 0)) {
// Сохраняем позицию скролла для текущего таба перед переходом
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer && mainTabs.includes(activeTab)) {
setScrollPositions(prev => ({
...prev,
[activeTab]: scrollContainer.scrollTop
}))
}
// Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab)
@@ -865,20 +854,6 @@ function AppContent() {
}
}, [selectedProject, activeTab])
// Восстанавливаем позицию скролла при возвращении на таб
useEffect(() => {
if (mainTabs.includes(activeTab) && scrollPositions[activeTab] !== undefined) {
// Используем setTimeout, чтобы DOM успел обновиться
const timeoutId = setTimeout(() => {
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer) {
scrollContainer.scrollTop = scrollPositions[activeTab]
}
}, 0)
return () => clearTimeout(timeoutId)
}
}, [activeTab, scrollPositions])
// Определяем общее состояние загрузки и ошибок для кнопки Refresh
@@ -897,45 +872,47 @@ function AppContent() {
// Определяем, нужно ли скрывать нижнюю панель (для fullscreen экранов)
const isFullscreenTab = activeTab === 'test' || activeTab === 'add-words' || activeTab === 'task-form' || activeTab === 'wishlist-form' || activeTab === 'wishlist-detail' || activeTab === 'todoist-integration' || activeTab === 'telegram-integration' || activeTab === 'full' || activeTab === 'priorities' || activeTab === 'words' || activeTab === 'dictionaries'
// Определяем отступы для контейнера
const getContainerPadding = () => {
if (!isFullscreenTab) {
// Для tasks, wishlist и profile на широких экранах увеличиваем отступ
if (activeTab === 'tasks' || activeTab === 'wishlist' || activeTab === 'profile') {
return 'p-4 md:p-8'
}
return 'p-4 md:p-6'
// Функция для получения классов скролл-контейнера для каждого таба
// Каждый таб имеет свой изолированный скролл-контейнер для автоматического сохранения позиции скролла
const getTabContainerClasses = (tabName) => {
const isActive = activeTab === tabName
const baseClasses = 'absolute inset-0 overflow-y-auto'
// Активный таб: z-10 (сверху), неактивные: z-0 + invisible + opacity-0 (мгновенное скрытие)
const visibilityClasses = isActive ? 'z-10' : 'z-0 invisible opacity-0 pointer-events-none'
// Определяем padding для каждого таба
let paddingClasses = ''
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
paddingClasses = 'pb-20'
} else if (tabName === 'words' || tabName === 'dictionaries') {
paddingClasses = 'pb-16'
}
// Для экрана статистики используем такие же отступы как для приоритетов
if (activeTab === 'full') {
return 'px-4 md:px-8 py-0'
return `${baseClasses} ${paddingClasses} ${visibilityClasses}`.trim()
}
// Функция для определения отступов внутреннего контейнера
const getInnerContainerClasses = (tabName) => {
if (tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
return 'max-w-7xl mx-auto p-4 md:p-8'
}
// Для экрана приоритетов используем такие же отступы как для profile
if (activeTab === 'priorities') {
return 'px-4 md:px-8 py-0'
if (tabName === 'current') {
return 'max-w-7xl mx-auto p-4 md:p-6'
}
// Для экрана словарей используем такие же отступы как для приоритетов
if (activeTab === 'dictionaries') {
return 'px-4 md:px-8 py-0'
if (tabName === 'full' || tabName === 'priorities' || tabName === 'dictionaries' || tabName === 'words') {
return 'max-w-7xl mx-auto px-4 md:px-8 py-0'
}
// Для экрана списка слов используем такие же отступы как для словарей
if (activeTab === 'words') {
return 'px-4 md:px-8 py-0'
}
// Для экрана желаний используем такие же отступы как для словарей
if (activeTab === 'wishlist') {
return 'px-4 md:px-8 py-0'
}
// Для остальных fullscreen экранов без отступов
return 'p-0'
// Fullscreen табы без отступов
return 'max-w-7xl mx-auto p-0'
}
return (
<div className="flex flex-col h-screen h-dvh overflow-hidden">
<div className={`flex-1 overflow-y-auto ${isFullscreenTab ? (activeTab === 'words' || activeTab === 'dictionaries' ? 'pb-16' : 'pb-0') : activeTab === 'tasks' || activeTab === 'wishlist' ? 'pb-20' : 'pb-20'}`}>
<div className={`max-w-7xl mx-auto ${getContainerPadding()}`}>
{loadedTabs.current && (
<div className={activeTab === 'current' ? 'block' : 'hidden'}>
{/* Контейнер табов - каждый таб имеет свой изолированный скролл */}
<div className="flex-1 relative overflow-hidden">
{loadedTabs.current && (
<div className={getTabContainerClasses('current')}>
<div className={getInnerContainerClasses('current')}>
<CurrentWeek
onProjectClick={handleProjectClick}
data={currentWeekData}
@@ -946,10 +923,12 @@ function AppContent() {
onNavigate={handleNavigate}
/>
</div>
)}
</div>
)}
{loadedTabs.priorities && (
<div className={activeTab === 'priorities' ? 'block' : 'hidden'}>
{loadedTabs.priorities && (
<div className={getTabContainerClasses('priorities')}>
<div className={getInnerContainerClasses('priorities')}>
<ProjectPriorityManager
allProjectsData={fullStatisticsData}
currentWeekData={currentWeekData}
@@ -960,10 +939,12 @@ function AppContent() {
onNavigate={handleNavigate}
/>
</div>
)}
</div>
)}
{loadedTabs.full && (
<div className={activeTab === 'full' ? 'block' : 'hidden'}>
{loadedTabs.full && (
<div className={getTabContainerClasses('full')}>
<div className={getInnerContainerClasses('full')}>
<FullStatistics
selectedProject={selectedProject}
onClearSelection={() => {
@@ -985,10 +966,12 @@ function AppContent() {
activeTab={activeTab}
/>
</div>
)}
</div>
)}
{loadedTabs.words && (
<div className={activeTab === 'words' ? 'block' : 'hidden'}>
{loadedTabs.words && (
<div className={getTabContainerClasses('words')}>
<div className={getInnerContainerClasses('words')}>
<WordList
onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId}
@@ -996,29 +979,35 @@ function AppContent() {
refreshTrigger={wordsRefreshTrigger}
/>
</div>
)}
</div>
)}
{loadedTabs['add-words'] && (
<div className={activeTab === 'add-words' ? 'block' : 'hidden'}>
{loadedTabs['add-words'] && (
<div className={getTabContainerClasses('add-words')}>
<div className={getInnerContainerClasses('add-words')}>
<AddWords
onNavigate={handleNavigate}
dictionaryId={tabParams.dictionaryId}
dictionaryName={tabParams.dictionaryName}
/>
</div>
)}
</div>
)}
{loadedTabs.dictionaries && (
<div className={activeTab === 'dictionaries' ? 'block' : 'hidden'}>
{loadedTabs.dictionaries && (
<div className={getTabContainerClasses('dictionaries')}>
<div className={getInnerContainerClasses('dictionaries')}>
<DictionaryList
onNavigate={handleNavigate}
refreshTrigger={dictionariesRefreshTrigger}
/>
</div>
)}
</div>
)}
{loadedTabs.test && (
<div className={activeTab === 'test' ? 'block' : 'hidden'}>
{loadedTabs.test && (
<div className={getTabContainerClasses('test')}>
<div className={getInnerContainerClasses('test')}>
<TestWords
onNavigate={handleNavigate}
wordCount={tabParams.wordCount}
@@ -1027,10 +1016,12 @@ function AppContent() {
taskId={tabParams.taskId}
/>
</div>
)}
</div>
)}
{loadedTabs.tasks && (
<div className={activeTab === 'tasks' ? 'block' : 'hidden'}>
{loadedTabs.tasks && (
<div className={getTabContainerClasses('tasks')}>
<div className={getInnerContainerClasses('tasks')}>
<TaskList
onNavigate={handleNavigate}
data={tasksData}
@@ -1041,10 +1032,12 @@ function AppContent() {
onRefresh={(isBackground = false) => fetchTasksData(isBackground)}
/>
</div>
)}
</div>
)}
{loadedTabs['task-form'] && (
<div className={activeTab === 'task-form' ? 'block' : 'hidden'}>
{loadedTabs['task-form'] && (
<div className={getTabContainerClasses('task-form')}>
<div className={getInnerContainerClasses('task-form')}>
<TaskForm
key={tabParams.taskId || 'new'}
onNavigate={handleNavigate}
@@ -1055,10 +1048,12 @@ function AppContent() {
returnWishlistId={tabParams.returnWishlistId}
/>
</div>
)}
</div>
)}
{loadedTabs.wishlist && (
<div className={activeTab === 'wishlist' ? 'block' : 'hidden'}>
{loadedTabs.wishlist && (
<div className={getTabContainerClasses('wishlist')}>
<div className={getInnerContainerClasses('wishlist')}>
<Wishlist
onNavigate={handleNavigate}
refreshTrigger={wishlistRefreshTrigger}
@@ -1067,10 +1062,12 @@ function AppContent() {
boardDeleted={tabParams.boardDeleted}
/>
</div>
)}
</div>
)}
{loadedTabs['wishlist-form'] && (
<div className={activeTab === 'wishlist-form' ? 'block' : 'hidden'}>
{loadedTabs['wishlist-form'] && (
<div className={getTabContainerClasses('wishlist-form')}>
<div className={getInnerContainerClasses('wishlist-form')}>
<WishlistForm
key={`${tabParams.wishlistId || 'new'}-${tabParams.editConditionIndex ?? ''}-${tabParams.newTaskId ?? ''}-${tabParams.boardId ?? ''}`}
onNavigate={handleNavigate}
@@ -1080,11 +1077,12 @@ function AppContent() {
boardId={tabParams.boardId}
/>
</div>
)}
</div>
)}
{loadedTabs['board-form'] && (
<div className={activeTab === 'board-form' ? 'block' : 'hidden'}>
{loadedTabs['board-form'] && (
<div className={getTabContainerClasses('board-form')}>
<div className={getInnerContainerClasses('board-form')}>
<BoardForm
key={tabParams.boardId || 'new'}
onNavigate={handleNavigate}
@@ -1092,36 +1090,44 @@ function AppContent() {
onSaved={() => setWishlistRefreshTrigger(prev => prev + 1)}
/>
</div>
)}
</div>
)}
{loadedTabs['board-join'] && (
<div className={activeTab === 'board-join' ? 'block' : 'hidden'}>
{loadedTabs['board-join'] && (
<div className={getTabContainerClasses('board-join')}>
<div className={getInnerContainerClasses('board-join')}>
<BoardJoinPreview
key={tabParams.inviteToken}
onNavigate={handleNavigate}
inviteToken={tabParams.inviteToken}
/>
</div>
)}
</div>
)}
{loadedTabs.profile && (
<div className={activeTab === 'profile' ? 'block' : 'hidden'}>
{loadedTabs.profile && (
<div className={getTabContainerClasses('profile')}>
<div className={getInnerContainerClasses('profile')}>
<Profile onNavigate={handleNavigate} />
</div>
)}
</div>
)}
{loadedTabs['todoist-integration'] && (
<div className={activeTab === 'todoist-integration' ? 'block' : 'hidden'}>
{loadedTabs['todoist-integration'] && (
<div className={getTabContainerClasses('todoist-integration')}>
<div className={getInnerContainerClasses('todoist-integration')}>
<TodoistIntegration onNavigate={handleNavigate} />
</div>
)}
</div>
)}
{loadedTabs['telegram-integration'] && (
<div className={activeTab === 'telegram-integration' ? 'block' : 'hidden'}>
{loadedTabs['telegram-integration'] && (
<div className={getTabContainerClasses('telegram-integration')}>
<div className={getInnerContainerClasses('telegram-integration')}>
<TelegramIntegration onNavigate={handleNavigate} />
</div>
)}
</div>
</div>
)}
</div>
{/* Кнопка добавления задачи (только для таба задач) */}