1
0
Files
TaskBoard/front_vue/src/views/ArchivePage.vue
Falknat 3258fa9137 Исправления фронта
Множество оптимизаций по фронту
2026-01-16 10:15:33 +07:00

407 lines
11 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>
<PageLayout>
<!-- Шапка с заголовком и фильтрами -->
<Header title="Архив задач">
<template #filters>
<DepartmentTags
v-model="activeDepartment"
@project-change="onProjectChange"
/>
</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" :class="{ mobile: isMobile }">
<!-- Мобильный заголовок над карточками -->
<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" :class="{ mobile: isMobile }">
<ArchiveCard
v-for="card in filteredCards"
:key="card.id"
:card="card"
@click="openTaskPanel(card)"
@restore="confirmRestore"
@delete="confirmDelete"
/>
<!-- Пустое состояние -->
<div v-if="filteredCards.length === 0 && !loading" class="empty-state" :class="{ mobile: isMobile }">
<i data-lucide="archive-x"></i>
<p>Архив пуст</p>
<span>Архивированные задачи появятся здесь</span>
</div>
<!-- Загрузка -->
<Loader v-if="loading" />
</div>
</main>
<!-- Модальные окна -->
<template #modals>
<TaskPanel
:show="panelOpen"
:card="editingCard"
:column-id="null"
:is-archived="true"
:on-save="handleSaveTask"
@close="closePanel"
@delete="handleDeleteTask"
@restore="handleRestoreFromPanel"
/>
<ConfirmDialog
:show="confirmDialogOpen"
type="deleteTask"
:action="handleConfirmDelete"
@confirm="confirmDialogOpen = false"
@cancel="confirmDialogOpen = false"
/>
<ConfirmDialog
:show="restoreDialogOpen"
type="restore"
:action="handleConfirmRestore"
@confirm="restoreDialogOpen = false"
@cancel="restoreDialogOpen = false"
/>
</template>
</PageLayout>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import PageLayout from '../components/PageLayout.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 DepartmentTags from '../components/DepartmentTags.vue'
import ProjectSelector from '../components/ProjectSelector.vue'
import MobileSelect from '../components/ui/MobileSelect.vue'
import Loader from '../components/ui/Loader.vue'
import { useProjectsStore } from '../stores/projects'
import { cardsApi } from '../api'
import { useMobile } from '../composables/useMobile'
import { useDepartmentFilter } from '../composables/useDepartmentFilter'
const { isMobile } = useMobile()
// ==================== STORE ====================
const store = useProjectsStore()
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
const { activeDepartment, departmentOptions, resetFilter } = useDepartmentFilter()
// ==================== КАРТОЧКИ ====================
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 () => {
resetFilter()
await fetchCards()
}
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
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 restoreDialogOpen = ref(false)
const cardToRestore = ref(null)
const confirmRestore = (cardId) => {
cardToRestore.value = cardId
restoreDialogOpen.value = true
}
const handleConfirmDelete = async () => {
if (!cardToDelete.value) {
throw new Error('Задача не выбрана')
}
const result = await cardsApi.delete(cardToDelete.value)
if (!result.success) {
throw new Error('Ошибка удаления задачи')
}
cards.value = cards.value.filter(c => c.id !== cardToDelete.value)
cardToDelete.value = null
}
// ==================== ВОССТАНОВЛЕНИЕ (действие) ====================
const handleConfirmRestore = async () => {
if (!cardToRestore.value) {
throw new Error('Задача не выбрана')
}
const result = await cardsApi.setArchive(cardToRestore.value, 0)
if (!result.success) {
throw new Error('Ошибка восстановления задачи')
}
cards.value = cards.value.filter(c => c.id !== cardToRestore.value)
cardToRestore.value = null
}
const handleRestoreFromPanel = async (cardId) => {
const result = await cardsApi.setArchive(cardId, 0)
if (result.success) {
cards.value = cards.value.filter(c => c.id !== cardId)
}
closePanel()
}
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(async () => {
await store.init()
await fetchCards()
if (window.lucide) window.lucide.createIcons()
})
</script>
<style scoped>
/* Специфичные стили для архива (вертикальный скролл) */
.main {
overflow-y: auto;
}
/* Мобильный архив — с padding */
.main.mobile {
padding: 0 16px 16px;
}
/* Список архивных карточек */
.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;
}
/* ========== MOBILE: Архив ========== */
.archive-list.mobile {
/* 60px header + 40px title + 64px nav + safe-area */
max-height: calc(100dvh - 60px - 40px - 64px - var(--safe-area-bottom, 0px));
max-width: none;
gap: 12px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
-ms-overflow-style: none;
padding-bottom: 16px;
}
.archive-list.mobile::-webkit-scrollbar {
display: none;
}
.empty-state.mobile {
padding: 40px 20px;
}
.empty-state.mobile 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>