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