Инициализация проекта
Загрузка проекта на GIT
This commit is contained in:
217
front_vue/src/components/Board.vue
Normal file
217
front_vue/src/components/Board.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="board">
|
||||
<div class="columns">
|
||||
<Column
|
||||
v-for="column in filteredColumns"
|
||||
:key="column.id"
|
||||
:column="column"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
@drop-card="handleDropCard"
|
||||
@open-task="(card) => emit('open-task', { card, columnId: column.id })"
|
||||
@create-task="emit('create-task', column.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUpdated, watch } from 'vue'
|
||||
import Column from './Column.vue'
|
||||
|
||||
const props = defineProps({
|
||||
activeDepartment: Number,
|
||||
departments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
labels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
cards: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['stats-updated', 'open-task', 'create-task'])
|
||||
|
||||
const refreshIcons = () => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refreshIcons)
|
||||
onUpdated(refreshIcons)
|
||||
|
||||
// Локальная копия карточек для optimistic UI
|
||||
const localCards = ref([])
|
||||
|
||||
// Синхронизируем с props при загрузке данных
|
||||
watch(() => props.cards, (newCards) => {
|
||||
if (newCards.length > 0 && localCards.value.length === 0) {
|
||||
// Первая загрузка - копируем данные и добавляем order если нет
|
||||
localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({
|
||||
...card,
|
||||
order: card.order ?? idx
|
||||
}))
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Собираем колонки с карточками (используем localCards, сортируем по order)
|
||||
const columnsWithCards = computed(() => {
|
||||
return props.columns.map(col => ({
|
||||
id: col.id,
|
||||
title: col.name,
|
||||
color: col.color,
|
||||
cards: localCards.value
|
||||
.filter(card => card.column_id === col.id)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(card => ({
|
||||
id: card.id_card,
|
||||
title: card.title,
|
||||
description: card.descript,
|
||||
details: card.descript_full,
|
||||
departmentId: card.id_department,
|
||||
labelId: card.id_label,
|
||||
assignee: card.avatar_img,
|
||||
dueDate: card.date,
|
||||
dateCreate: card.date_create,
|
||||
files: card.files || (card.file_img || []).map(f => ({
|
||||
name: f.name,
|
||||
url: f.url,
|
||||
size: f.size,
|
||||
preview: f.url
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
// Фильтруем колонки по активному отделу
|
||||
const filteredColumns = computed(() => {
|
||||
if (!props.activeDepartment) return columnsWithCards.value
|
||||
|
||||
return columnsWithCards.value.map(col => ({
|
||||
...col,
|
||||
cards: col.cards.filter(card => card.departmentId === props.activeDepartment)
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredTotalTasks = computed(() => {
|
||||
return filteredColumns.value.reduce((sum, col) => sum + col.cards.length, 0)
|
||||
})
|
||||
|
||||
const inProgressTasks = computed(() => {
|
||||
const col = filteredColumns.value.find(c => c.id === 3) // В работе
|
||||
return col ? col.cards.length : 0
|
||||
})
|
||||
|
||||
const completedTasks = computed(() => {
|
||||
const col = filteredColumns.value.find(c => c.id === 5) // Готово
|
||||
return col ? col.cards.length : 0
|
||||
})
|
||||
|
||||
// Отправляем статистику в родителя
|
||||
watch([filteredTotalTasks, inProgressTasks, completedTasks], () => {
|
||||
emit('stats-updated', {
|
||||
total: filteredTotalTasks.value,
|
||||
inProgress: inProgressTasks.value,
|
||||
done: completedTasks.value
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
const handleDropCard = ({ cardId, fromColumnId, toColumnId, toIndex }) => {
|
||||
const card = localCards.value.find(c => c.id_card === cardId)
|
||||
if (!card) return
|
||||
|
||||
// Меняем колонку
|
||||
card.column_id = toColumnId
|
||||
|
||||
// Получаем карточки целевой колонки (без перемещаемой)
|
||||
const columnCards = localCards.value
|
||||
.filter(c => c.column_id === toColumnId && c.id_card !== cardId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
|
||||
// Вставляем карточку в нужную позицию и пересчитываем order
|
||||
columnCards.splice(toIndex, 0, card)
|
||||
columnCards.forEach((c, idx) => {
|
||||
c.order = idx
|
||||
})
|
||||
|
||||
// TODO: отправить изменение на сервер
|
||||
}
|
||||
|
||||
// Генератор id для новых карточек
|
||||
let nextCardId = 100
|
||||
|
||||
// Методы для модалки
|
||||
const saveTask = (taskData, columnId) => {
|
||||
if (taskData.id) {
|
||||
// Редактирование существующей карточки
|
||||
const card = localCards.value.find(c => c.id_card === taskData.id)
|
||||
if (card) {
|
||||
card.title = taskData.title
|
||||
card.descript = taskData.description
|
||||
card.descript_full = taskData.details
|
||||
card.id_department = taskData.departmentId
|
||||
card.id_label = taskData.labelId
|
||||
card.date = taskData.dueDate
|
||||
card.avatar_img = taskData.assignee
|
||||
card.files = taskData.files || []
|
||||
}
|
||||
} else {
|
||||
// Создание новой карточки (в конец колонки)
|
||||
const columnCards = localCards.value.filter(c => c.column_id === columnId)
|
||||
const maxOrder = columnCards.length > 0
|
||||
? Math.max(...columnCards.map(c => c.order)) + 1
|
||||
: 0
|
||||
|
||||
localCards.value.push({
|
||||
id_card: nextCardId++,
|
||||
id_department: taskData.departmentId,
|
||||
id_label: taskData.labelId,
|
||||
title: taskData.title,
|
||||
descript: taskData.description,
|
||||
descript_full: taskData.details,
|
||||
avatar_img: taskData.assignee,
|
||||
column_id: columnId,
|
||||
date: taskData.dueDate,
|
||||
date_create: new Date().toISOString().split('T')[0],
|
||||
order: maxOrder,
|
||||
files: taskData.files || []
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: отправить на сервер
|
||||
}
|
||||
|
||||
const deleteTask = (cardId, columnId) => {
|
||||
const index = localCards.value.findIndex(c => c.id_card === cardId)
|
||||
if (index !== -1) {
|
||||
localCards.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// TODO: отправить на сервер
|
||||
}
|
||||
|
||||
defineExpose({ saveTask, deleteTask })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.board {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user