282 lines
6.0 KiB
Vue
282 lines
6.0 KiB
Vue
<template>
|
|
<div
|
|
class="card"
|
|
draggable="true"
|
|
@dragstart="handleDragStart"
|
|
@dragend="handleDragEnd"
|
|
:class="{ dragging: isDragging, 'has-label-color': cardLabelColor }"
|
|
:style="cardLabelColor ? { '--label-bg': cardLabelColor } : {}"
|
|
>
|
|
<div class="card-header">
|
|
<div class="tag-group">
|
|
<span v-if="cardLabel" class="label-icon">{{ cardLabel.icon }}</span>
|
|
<span
|
|
v-if="cardDepartment"
|
|
class="tag"
|
|
:style="{ color: cardDepartment.color }"
|
|
>
|
|
{{ cardDepartment.name }}
|
|
</span>
|
|
</div>
|
|
<div class="header-right">
|
|
<span v-if="card.files && card.files.length" class="files-indicator" :title="card.files.length + ' изображений'">
|
|
<i data-lucide="image-plus"></i>
|
|
</span>
|
|
<div v-if="card.assignee" class="assignee">
|
|
<img
|
|
v-if="isAvatarUrl(card.assignee)"
|
|
:src="card.assignee"
|
|
alt="avatar"
|
|
class="assignee-img"
|
|
/>
|
|
<span v-else>{{ card.assignee }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 class="card-title">{{ card.title }}</h3>
|
|
|
|
<p v-if="card.description" class="card-description">
|
|
{{ card.description }}
|
|
</p>
|
|
|
|
<div class="card-footer">
|
|
<span v-if="card.dateCreate" class="date-create">
|
|
Создано: {{ formatDateWithYear(card.dateCreate) }}
|
|
</span>
|
|
<span v-if="card.dueDate" class="due-date" :class="dueDateStatus">
|
|
{{ daysLeftText }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUpdated } from 'vue'
|
|
|
|
const props = defineProps({
|
|
card: Object,
|
|
columnId: [String, Number],
|
|
index: Number,
|
|
departments: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
labels: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
})
|
|
|
|
// defineEmits(['delete']) - будет при раскрытии карточки
|
|
|
|
const refreshIcons = () => {
|
|
if (window.lucide) {
|
|
window.lucide.createIcons()
|
|
}
|
|
}
|
|
|
|
onMounted(refreshIcons)
|
|
onUpdated(refreshIcons)
|
|
|
|
const isDragging = ref(false)
|
|
|
|
const handleDragStart = (e) => {
|
|
isDragging.value = true
|
|
e.dataTransfer.effectAllowed = 'move'
|
|
e.dataTransfer.setData('cardId', props.card.id.toString())
|
|
e.dataTransfer.setData('columnId', props.columnId)
|
|
}
|
|
|
|
const handleDragEnd = () => {
|
|
isDragging.value = false
|
|
}
|
|
|
|
// Получаем отдел по id
|
|
const cardDepartment = computed(() => {
|
|
if (!props.card.departmentId) return null
|
|
return props.departments.find(d => d.id === props.card.departmentId) || null
|
|
})
|
|
|
|
// Получаем лейбл по id
|
|
const cardLabel = computed(() => {
|
|
if (!props.card.labelId) return null
|
|
return props.labels.find(l => l.id === props.card.labelId) || null
|
|
})
|
|
|
|
// Цвет лейбла для фона карточки
|
|
const cardLabelColor = computed(() => {
|
|
return cardLabel.value?.color || null
|
|
})
|
|
|
|
const formatDateWithYear = (dateStr) => {
|
|
const date = new Date(dateStr)
|
|
const day = date.getDate()
|
|
const months = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']
|
|
return `${day} ${months[date.getMonth()]} ${date.getFullYear()}`
|
|
}
|
|
|
|
const getDaysLeft = () => {
|
|
if (!props.card.dueDate) return null
|
|
const due = new Date(props.card.dueDate)
|
|
const today = new Date()
|
|
today.setHours(0, 0, 0, 0)
|
|
due.setHours(0, 0, 0, 0)
|
|
return Math.round((due - today) / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
const dueDateStatus = computed(() => {
|
|
const days = getDaysLeft()
|
|
if (days === null) return ''
|
|
if (days < 0) return 'overdue'
|
|
if (days <= 2) return 'soon'
|
|
return ''
|
|
})
|
|
|
|
const daysLeftText = computed(() => {
|
|
const days = getDaysLeft()
|
|
if (days === null) return ''
|
|
if (days < 0) return `Просрочено: ${Math.abs(days)} дн.`
|
|
if (days === 0) return 'Сегодня'
|
|
if (days === 1) return 'Завтра'
|
|
return `Осталось: ${days} дн.`
|
|
})
|
|
|
|
const isAvatarUrl = (value) => {
|
|
return value && (value.startsWith('http://') || value.startsWith('https://'))
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card {
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
cursor: grab;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.card:hover {
|
|
background: var(--bg-card-hover);
|
|
}
|
|
|
|
.card.has-label-color {
|
|
background: color-mix(in srgb, var(--label-bg) 15%, var(--bg-card));
|
|
border-left: 3px solid var(--label-bg);
|
|
}
|
|
|
|
.card.has-label-color:hover {
|
|
background: color-mix(in srgb, var(--label-bg) 20%, var(--bg-card-hover));
|
|
}
|
|
|
|
.card.dragging {
|
|
opacity: 0.4;
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.tag-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.label-icon {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.tag {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.4px;
|
|
}
|
|
|
|
.assignee {
|
|
width: 24px;
|
|
height: 24px;
|
|
background: var(--blue);
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 8px;
|
|
font-weight: 600;
|
|
color: white;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.assignee-img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
|
|
.card-title {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
line-height: 1.5;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.card-description {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.card-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 12px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
}
|
|
|
|
.date-create {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.files-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.files-indicator i {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.due-date {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.due-date.soon {
|
|
color: var(--orange);
|
|
}
|
|
|
|
.due-date.overdue {
|
|
color: var(--red);
|
|
}
|
|
</style>
|