1
0

Инициализация проекта

Загрузка проекта на GIT
This commit is contained in:
2026-01-11 15:01:35 +07:00
commit 301e179160
28 changed files with 7769 additions and 0 deletions

View 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>