1
0
Files
TaskBoard/front_vue/src/views/MainApp.vue
Falknat 8ac497df63 Авто Обновление данных
сделал автоматическое обновление данных по таскам у других клиентов.
2026-01-15 04:52:19 +07:00

333 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app">
<!-- Боковая панель навигации -->
<Sidebar />
<!-- Основной контент -->
<div class="main-wrapper">
<!-- Шапка с заголовком, фильтрами и статистикой -->
<Header title="Доска задач">
<template #filters>
<div class="filters">
<!-- Выбор проекта -->
<ProjectSelector @change="onProjectChange" />
<div class="filter-divider"></div>
<!-- Фильтр по отделам -->
<button
class="filter-tag"
:class="{ active: activeDepartment === null }"
@click="activeDepartment = null"
>
Все
</button>
<button
v-for="dept in store.departments"
:key="dept.id"
class="filter-tag"
:class="{ active: activeDepartment === dept.id }"
@click="activeDepartment = activeDepartment === dept.id ? null : dept.id"
>
{{ dept.name_departments }}
</button>
</div>
</template>
<template #stats>
<div class="header-stats">
<div class="stat">
<span class="stat-value">{{ stats.total }}</span>
<span class="stat-label">задач</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-value">{{ stats.inProgress }}</span>
<span class="stat-label">в работе</span>
</div>
<div class="stat-divider"></div>
<div class="stat">
<span class="stat-value">{{ stats.done }}</span>
<span class="stat-label">готово</span>
</div>
</div>
</template>
</Header>
<!-- Доска с колонками и карточками -->
<main class="main">
<Board
ref="boardRef"
:active-department="activeDepartment"
:departments="store.departments"
:labels="store.labels"
:columns="store.columns"
:cards="cards"
:done-column-id="store.doneColumnId"
@stats-updated="stats = $event"
@open-task="openTaskPanel"
@create-task="openNewTaskPanel"
/>
</main>
</div>
<!-- Панель редактирования/создания задачи -->
<TaskPanel
:show="panelOpen"
:card="editingCard"
:column-id="editingColumnId"
:done-column-id="store.doneColumnId"
:departments="store.departments"
:labels="store.labels"
:users="store.users"
@close="closePanel"
@save="handleSaveTask"
@delete="handleDeleteTask"
@archive="handleArchiveTask"
/>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import Sidebar from '../components/Sidebar.vue'
import Header from '../components/Header.vue'
import Board from '../components/Board.vue'
import TaskPanel from '../components/TaskPanel.vue'
import ProjectSelector from '../components/ProjectSelector.vue'
import { useProjectsStore } from '../stores/projects'
import { cardsApi } from '../api'
// ==================== STORE ====================
const store = useProjectsStore()
// ==================== КАРТОЧКИ ====================
const cards = ref([])
// Загрузка карточек текущего проекта
const fetchCards = async () => {
if (!store.currentProjectId) return
const result = await cardsApi.getAll(store.currentProjectId)
if (result.success) cards.value = result.data
}
// При смене проекта — перезагружаем карточки
const onProjectChange = async () => {
activeDepartment.value = null
await fetchCards()
}
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
const savedDepartment = localStorage.getItem('activeDepartment')
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
watch(activeDepartment, (newVal) => {
if (newVal === null) {
localStorage.removeItem('activeDepartment')
} else {
localStorage.setItem('activeDepartment', newVal.toString())
}
})
// ==================== СТАТИСТИКА ====================
const stats = ref({ total: 0, inProgress: 0, done: 0 })
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
const boardRef = ref(null)
const panelOpen = ref(false)
const editingCard = ref(null)
const editingColumnId = ref(null)
const openTaskPanel = ({ card, columnId }) => {
editingCard.value = card
editingColumnId.value = columnId
panelOpen.value = true
}
const openNewTaskPanel = (columnId) => {
editingCard.value = null
editingColumnId.value = columnId
panelOpen.value = true
}
const closePanel = () => {
panelOpen.value = false
editingCard.value = null
editingColumnId.value = null
}
const handleSaveTask = async (taskData) => {
if (!taskData.id && store.currentProjectId) {
taskData.id_project = store.currentProjectId
}
await boardRef.value?.saveTask(taskData, editingColumnId.value)
closePanel()
}
const handleDeleteTask = async (cardId) => {
await boardRef.value?.deleteTask(cardId, editingColumnId.value)
closePanel()
}
const handleArchiveTask = async (cardId) => {
await boardRef.value?.archiveTask(cardId)
closePanel()
}
// ==================== АВТООБНОВЛЕНИЕ (POLLING) ====================
const REFRESH_INTERVAL = (window.APP_CONFIG?.IDLE_REFRESH_SECONDS ?? 30) * 1000
let pollTimer = null
const startPolling = () => {
if (pollTimer) clearInterval(pollTimer)
pollTimer = setInterval(async () => {
console.log('[AutoRefresh] Обновление данных...')
await fetchCards()
}, REFRESH_INTERVAL)
}
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(async () => {
await store.init()
await fetchCards()
startPolling()
if (window.lucide) window.lucide.createIcons()
})
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
/* Контейнер приложения */
.app {
display: flex;
min-height: 100vh;
}
/* Основная область контента */
.main-wrapper {
flex: 1;
margin-left: 64px;
display: flex;
flex-direction: column;
background: var(--bg-main);
}
/* Контейнер фильтров */
.filters {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* Разделитель между проектом и отделами */
.filter-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
margin: 0 8px;
}
/* Кнопка фильтра */
.filter-tag {
padding: 6px 12px;
background: var(--bg-card);
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-family: inherit;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
cursor: pointer;
transition: all 0.15s;
}
.filter-tag:hover {
background: var(--bg-card-hover);
color: var(--text-primary);
}
/* Активный фильтр */
.filter-tag.active {
background: var(--accent);
color: #000;
}
/* Блок статистики */
.header-stats {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 20px;
background: var(--bg-card);
border-radius: 10px;
}
.stat {
display: flex;
align-items: baseline;
gap: 5px;
}
.stat-value {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.stat-label {
font-size: 12px;
color: var(--text-muted);
}
/* Разделитель между статами */
.stat-divider {
width: 1px;
height: 16px;
background: rgba(255, 255, 255, 0.1);
}
/* Основная область с доской */
.main {
flex: 1;
padding: 0 36px 36px;
overflow-x: auto;
scroll-behavior: smooth;
}
/* Стилизация горизонтального скроллбара */
.main::-webkit-scrollbar {
height: 10px;
}
.main::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.08);
border-radius: 5px;
margin: 0 36px;
}
.main::-webkit-scrollbar-thumb {
background: rgba(0, 212, 170, 0.4);
border-radius: 5px;
border: 2px solid transparent;
background-clip: padding-box;
}
.main::-webkit-scrollbar-thumb:hover {
background: rgba(0, 212, 170, 0.6);
}
</style>