Инициализация проекта
Загрузка проекта на GIT
This commit is contained in:
243
front_vue/src/components/Column.vue
Normal file
243
front_vue/src/components/Column.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<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)"
|
||||
/>
|
||||
</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'])
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user