1
0
Files
TaskBoard/front_vue/src/views/ArchivePage.vue
Falknat 7e1482f515 Исправление ошибок фронта
Правим фронт от ошибок
2026-01-15 15:27:39 +07:00

584 lines
14 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" :class="{ mobile: isMobile }">
<!-- Боковая панель навигации -->
<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 #mobile-filters>
<ProjectSelector @change="onProjectChange" />
<MobileSelect
v-model="activeDepartment"
:options="departmentOptions"
title="Фильтр по отделам"
placeholder="Все отделы"
icon="filter"
compact
/>
</template>
<template #stats>
<div class="header-stats">
<div class="stat">
<span class="stat-value">{{ filteredCards.length }}</span>
<span class="stat-label">в архиве</span>
</div>
</div>
</template>
</Header>
<!-- Список архивных задач -->
<main class="main">
<!-- Мобильный заголовок над карточками -->
<div v-if="isMobile" class="mobile-archive-header">
<div class="archive-title-row">
<span class="archive-dot"></span>
<span class="archive-title">В архиве</span>
<span class="archive-count">{{ filteredCards.length }}</span>
</div>
</div>
<div class="archive-list">
<ArchiveCard
v-for="card in filteredCards"
:key="card.id"
:card="card"
:departments="store.departments"
:labels="store.labels"
@click="openTaskPanel(card)"
@restore="handleRestore"
@delete="confirmDelete"
/>
<!-- Пустое состояние -->
<div v-if="filteredCards.length === 0 && !loading" class="empty-state">
<i data-lucide="archive-x"></i>
<p>Архив пуст</p>
<span>Архивированные задачи появятся здесь</span>
</div>
<!-- Загрузка -->
<div v-if="loading" class="loading-state">
<i data-lucide="loader-2" class="spin"></i>
<span>Загрузка...</span>
</div>
</div>
</main>
</div>
<!-- Панель редактирования задачи -->
<TaskPanel
:show="panelOpen"
:card="editingCard"
:column-id="null"
:is-archived="true"
:departments="store.departments"
:labels="store.labels"
:users="store.users"
:current-user-id="store.currentUserId"
:is-project-admin="store.isProjectAdmin"
:on-save="handleSaveTask"
@close="closePanel"
@delete="handleDeleteTask"
@restore="handleRestoreFromPanel"
/>
<!-- Диалог подтверждения удаления -->
<ConfirmDialog
:show="confirmDialogOpen"
title="Удалить задачу?"
message="Задача будет удалена безвозвратно. Это действие нельзя отменить."
confirm-text="Удалить"
variant="danger"
:is-loading="isDeleting"
@confirm="handleConfirmDelete"
@cancel="confirmDialogOpen = false"
/>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import Sidebar from '../components/Sidebar.vue'
import Header from '../components/Header.vue'
import ArchiveCard from '../components/ArchiveCard.vue'
import TaskPanel from '../components/TaskPanel'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import ProjectSelector from '../components/ProjectSelector.vue'
import MobileSelect from '../components/ui/MobileSelect.vue'
import { useProjectsStore } from '../stores/projects'
import { cardsApi } from '../api'
import { useMobile } from '../composables/useMobile'
const { isMobile } = useMobile()
// ==================== СОСТОЯНИЯ ЗАГРУЗКИ ====================
const isRestoring = ref(false)
const isDeleting = ref(false)
// ==================== STORE ====================
const store = useProjectsStore()
// ==================== MOBILE ====================
const departmentOptions = computed(() => [
{ id: null, label: 'Все отделы' },
...store.departments.map(d => ({
id: d.id,
label: d.name_departments
}))
])
// ==================== КАРТОЧКИ ====================
const cards = ref([])
const loading = ref(true)
// Отфильтрованные карточки (сортируем по дате завершения, новые сверху)
const filteredCards = computed(() => {
let result = cards.value
if (activeDepartment.value) {
result = result.filter(card => card.departmentId === activeDepartment.value)
}
return result.sort((a, b) => {
const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0
const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0
return dateB - dateA
})
})
// Загрузка архивных карточек
const fetchCards = async () => {
if (!store.currentProjectId) {
loading.value = false
return
}
loading.value = true
try {
const result = await cardsApi.getAll(store.currentProjectId, 1) // archive = 1
if (result.success) {
cards.value = result.data.map(card => ({
id: card.id,
title: card.title,
description: card.descript,
details: card.descript_full,
departmentId: card.id_department,
labelId: card.id_label,
accountId: card.id_account,
assignee: card.avatar_img,
dueDate: card.date,
dateCreate: card.date_create,
dateClosed: card.date_closed,
columnId: card.column_id,
order: card.order ?? 0,
comments_count: card.comments_count || 0,
files: card.files || (card.file_img || []).map(f => ({
name: f.name,
url: f.url,
size: f.size,
preview: f.url
}))
}))
}
} finally {
loading.value = false
}
}
// При смене проекта
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 panelOpen = ref(false)
const editingCard = ref(null)
const openTaskPanel = (card) => {
editingCard.value = card
panelOpen.value = true
}
const closePanel = () => {
panelOpen.value = false
editingCard.value = null
}
const handleSaveTask = async (taskData) => {
if (taskData.id) {
await cardsApi.update({
id: taskData.id,
id_department: taskData.departmentId,
id_label: taskData.labelId,
id_account: taskData.accountId,
date: taskData.dueDate,
title: taskData.title,
descript: taskData.description,
descript_full: taskData.details
})
const card = cards.value.find(c => c.id === taskData.id)
if (card) {
card.title = taskData.title
card.description = taskData.description
card.details = taskData.details
card.departmentId = taskData.departmentId
card.labelId = taskData.labelId
card.dueDate = taskData.dueDate
card.accountId = taskData.accountId
card.assignee = taskData.assignee
card.files = taskData.files || []
}
}
closePanel()
}
const handleDeleteTask = async (cardId) => {
const result = await cardsApi.delete(cardId)
if (result.success) {
cards.value = cards.value.filter(c => c.id !== cardId)
}
closePanel()
}
// ==================== УДАЛЕНИЕ С ПОДТВЕРЖДЕНИЕМ ====================
const confirmDialogOpen = ref(false)
const cardToDelete = ref(null)
const confirmDelete = (cardId) => {
cardToDelete.value = cardId
confirmDialogOpen.value = true
}
const handleConfirmDelete = async () => {
if (isDeleting.value || !cardToDelete.value) return
isDeleting.value = true
try {
const result = await cardsApi.delete(cardToDelete.value)
if (result.success) {
cards.value = cards.value.filter(c => c.id !== cardToDelete.value)
}
confirmDialogOpen.value = false
cardToDelete.value = null
} finally {
isDeleting.value = false
}
}
// ==================== ВОССТАНОВЛЕНИЕ ====================
const handleRestore = async (cardId) => {
if (isRestoring.value) return
isRestoring.value = true
try {
const result = await cardsApi.setArchive(cardId, 0)
if (result.success) {
cards.value = cards.value.filter(c => c.id !== cardId)
}
} finally {
isRestoring.value = false
}
}
const handleRestoreFromPanel = async (cardId) => {
await handleRestore(cardId)
closePanel()
}
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(async () => {
await store.init()
await fetchCards()
if (window.lucide) window.lucide.createIcons()
})
</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);
}
/* Основная область */
.main {
flex: 1;
padding: 0 36px 36px;
overflow-y: auto;
}
/* Список архивных карточек */
.archive-list {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 1200px;
min-height: 200px;
}
/* Пустое состояние */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
color: var(--text-muted);
}
.empty-state i {
width: 48px;
height: 48px;
opacity: 0.4;
margin-bottom: 16px;
}
.empty-state p {
font-size: 16px;
font-weight: 500;
color: var(--text-secondary);
margin: 0 0 8px 0;
}
.empty-state span {
font-size: 13px;
}
/* Загрузка */
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px 20px;
color: var(--text-muted);
}
.loading-state i {
width: 20px;
height: 20px;
}
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ========== MOBILE ========== */
.app.mobile {
flex-direction: column;
height: 100vh;
height: 100dvh;
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.app.mobile .main-wrapper {
margin-left: 0;
flex: 1;
min-height: 0;
padding-bottom: calc(64px + var(--safe-area-bottom, 0px));
overflow: hidden;
display: flex;
flex-direction: column;
}
.app.mobile .main {
flex: 1;
min-height: 0;
padding: 0 16px 16px;
overflow: hidden;
}
.app.mobile .archive-list {
/* 60px header + 40px title + 64px nav + safe-area */
max-height: calc(100dvh - 60px - 40px - 64px - var(--safe-area-bottom, 0px));
overflow-y: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
padding-bottom: 16px;
}
.app.mobile .archive-list::-webkit-scrollbar {
display: none;
}
.app.mobile .archive-list {
max-width: none;
gap: 12px;
}
.app.mobile .empty-state {
padding: 40px 20px;
}
.app.mobile .empty-state i {
width: 40px;
height: 40px;
}
/* Мобильный заголовок над карточками */
.mobile-archive-header {
padding: 12px 0 16px 4px;
}
.archive-title-row {
display: flex;
align-items: center;
gap: 8px;
}
.archive-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--orange, #ff9f43);
}
.archive-title {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
.archive-count {
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
}
</style>