1. Переписал модуль выпадающего слева меню 2. Добавил механику Архивации задач 3. Запоминания выбранного отдела
245 lines
4.8 KiB
Vue
245 lines
4.8 KiB
Vue
<template>
|
|
<div
|
|
class="column"
|
|
@dragover.prevent="handleDragOver"
|
|
@dragenter.prevent="handleDragEnter"
|
|
@dragleave="handleDragLeave"
|
|
@drop="handleDrop"
|
|
:class="{ 'drag-over': isDragOver }"
|
|
ref="columnRef"
|
|
>
|
|
<div class="column-header">
|
|
<div class="column-title-row">
|
|
<span class="column-dot" :style="{ background: column.color }"></span>
|
|
<h2 class="column-title">{{ column.title }}</h2>
|
|
<span class="column-count">{{ column.cards.length }}</span>
|
|
</div>
|
|
<button class="column-add" @click="emit('create-task')">
|
|
<i data-lucide="plus"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="cards" ref="cardsRef">
|
|
<template v-for="(card, index) in column.cards" :key="card.id">
|
|
<!-- Индикатор перед карточкой -->
|
|
<div v-if="isDragOver && dropIndex === index" class="drop-indicator"></div>
|
|
<Card
|
|
:card="card"
|
|
:column-id="column.id"
|
|
:index="index"
|
|
:departments="departments"
|
|
:labels="labels"
|
|
@click="emit('open-task', card)"
|
|
@archive="emit('archive-task', $event)"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Индикатор в конце списка -->
|
|
<div v-if="isDragOver && dropIndex === column.cards.length" class="drop-indicator"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUpdated } from 'vue'
|
|
import Card from './Card.vue'
|
|
|
|
const props = defineProps({
|
|
column: Object,
|
|
departments: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
labels: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['drop-card', 'open-task', 'create-task', 'archive-task'])
|
|
|
|
const refreshIcons = () => {
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
}
|
|
|
|
onMounted(refreshIcons)
|
|
onUpdated(refreshIcons)
|
|
|
|
const columnRef = ref(null)
|
|
const cardsRef = ref(null)
|
|
const isDragOver = ref(false)
|
|
const dropIndex = ref(-1)
|
|
let dragEnterCounter = 0
|
|
|
|
const handleDragEnter = (e) => {
|
|
dragEnterCounter++
|
|
isDragOver.value = true
|
|
}
|
|
|
|
const calculateDropIndex = (clientY) => {
|
|
if (!cardsRef.value) return props.column.cards.length
|
|
|
|
const cardElements = cardsRef.value.querySelectorAll('.card')
|
|
let index = props.column.cards.length
|
|
|
|
for (let i = 0; i < cardElements.length; i++) {
|
|
const rect = cardElements[i].getBoundingClientRect()
|
|
const cardMiddle = rect.top + rect.height / 2
|
|
|
|
if (clientY < cardMiddle) {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
|
|
return index
|
|
}
|
|
|
|
const handleDragOver = (e) => {
|
|
isDragOver.value = true
|
|
dropIndex.value = calculateDropIndex(e.clientY)
|
|
}
|
|
|
|
const handleDragLeave = (e) => {
|
|
dragEnterCounter--
|
|
if (dragEnterCounter === 0) {
|
|
isDragOver.value = false
|
|
dropIndex.value = -1
|
|
}
|
|
}
|
|
|
|
const handleDrop = (e) => {
|
|
const cardId = parseInt(e.dataTransfer.getData('cardId'))
|
|
const fromColumnId = e.dataTransfer.getData('columnId')
|
|
const toIndex = calculateDropIndex(e.clientY)
|
|
|
|
dragEnterCounter = 0
|
|
isDragOver.value = false
|
|
dropIndex.value = -1
|
|
|
|
emit('drop-card', {
|
|
cardId,
|
|
fromColumnId,
|
|
toColumnId: props.column.id,
|
|
toIndex
|
|
})
|
|
}
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
.column {
|
|
width: 300px;
|
|
min-width: 300px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: calc(100vh - 140px);
|
|
}
|
|
|
|
.column.drag-over .cards {
|
|
background: var(--accent-soft);
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.column-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.column-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.column-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.column-title {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.column-count {
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.column-add {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
padding: 6px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.column-add i {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.column-add:hover {
|
|
background: var(--bg-card);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.cards {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: 4px;
|
|
margin: -4px;
|
|
min-height: 150px;
|
|
}
|
|
|
|
.cards::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.cards::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.cards::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.cards::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}
|
|
|
|
.drop-indicator {
|
|
height: 4px;
|
|
background: var(--accent);
|
|
border-radius: 2px;
|
|
margin: 4px 0;
|
|
animation: pulse 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.5; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
</style>
|