1
0

Правка фронта

This commit is contained in:
2026-01-14 08:22:01 +07:00
parent 7eb50ed503
commit 04e88cb7fa
5 changed files with 108 additions and 200 deletions

View File

@@ -1,21 +1,10 @@
<template> <template>
<div <div
class="archive-card" class="archive-card"
:class="{ 'has-label-color': cardLabelColor, 'dragging': isDragging }" :class="{ 'has-label-color': cardLabelColor }"
:style="cardLabelColor ? { '--label-bg': cardLabelColor } : {}" :style="cardLabelColor ? { '--label-bg': cardLabelColor } : {}"
:draggable="true"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
@click="$emit('click')" @click="$emit('click')"
> >
<!-- Drag handle (6 точек) -->
<div
class="drag-handle"
title="Перетащите для изменения порядка"
>
<i data-lucide="grip-vertical"></i>
</div>
<!-- Аватарка --> <!-- Аватарка -->
<div class="card-assignee" v-if="card.assignee"> <div class="card-assignee" v-if="card.assignee">
<img <img
@@ -79,12 +68,11 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUpdated } from 'vue' import { computed, onMounted, onUpdated } from 'vue'
import { getFullUrl } from '../api' import { getFullUrl } from '../api'
const props = defineProps({ const props = defineProps({
card: Object, card: Object,
index: Number,
departments: { departments: {
type: Array, type: Array,
default: () => [] default: () => []
@@ -95,7 +83,7 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['click', 'restore', 'delete', 'dragstart', 'dragend']) const emit = defineEmits(['click', 'restore', 'delete'])
const refreshIcons = () => { const refreshIcons = () => {
if (window.lucide) { if (window.lucide) {
@@ -106,22 +94,6 @@ const refreshIcons = () => {
onMounted(refreshIcons) onMounted(refreshIcons)
onUpdated(refreshIcons) onUpdated(refreshIcons)
// Drag state
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('fromIndex', props.index.toString())
emit('dragstart', props.card.id)
}
const handleDragEnd = () => {
isDragging.value = false
emit('dragend')
}
// Получаем отдел по id // Получаем отдел по id
const cardDepartment = computed(() => { const cardDepartment = computed(() => {
if (!props.card.departmentId) return null if (!props.card.departmentId) return null
@@ -174,40 +146,6 @@ const formatDateFull = (dateStr) => {
background: var(--bg-card-hover); background: var(--bg-card-hover);
} }
.archive-card.dragging {
opacity: 0.4;
}
/* Drag handle (6 точек) */
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
flex-shrink: 0;
padding: 4px;
margin: -4px;
margin-right: 0;
border-radius: 4px;
color: var(--text-muted);
opacity: 0.4;
transition: all 0.15s ease;
}
.drag-handle:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.06);
}
.drag-handle:active {
cursor: grabbing;
}
.drag-handle i {
width: 16px;
height: 16px;
}
.archive-card.has-label-color { .archive-card.has-label-color {
border-left-color: var(--label-bg); border-left-color: var(--label-bg);
background: color-mix(in srgb, var(--label-bg) 8%, var(--bg-card)); background: color-mix(in srgb, var(--label-bg) 8%, var(--bg-card));

View File

@@ -46,7 +46,9 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted, onUpdated } from 'vue' import { ref, watch, onMounted, onUpdated, onUnmounted } from 'vue'
const STORAGE_KEY = 'taskboard_editor_height'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -70,6 +72,22 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'focus', 'blur']) const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
const editorRef = ref(null) const editorRef = ref(null)
let resizeObserver = null
// Получить сохранённую высоту из localStorage
function getSavedHeight() {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
const height = parseInt(saved, 10)
if (!isNaN(height) && height >= 120) return height
}
return null
}
// Сохранить высоту в localStorage
function saveHeight(height) {
localStorage.setItem(STORAGE_KEY, String(height))
}
// Применить форматирование // Применить форматирование
const applyFormat = (command) => { const applyFormat = (command) => {
@@ -202,10 +220,36 @@ const refreshIcons = () => {
onMounted(() => { onMounted(() => {
refreshIcons() refreshIcons()
setContent(props.modelValue) setContent(props.modelValue)
// Восстановить сохранённую высоту
const savedHeight = getSavedHeight()
if (savedHeight && editorRef.value) {
editorRef.value.style.height = savedHeight + 'px'
}
// Следить за изменением размера и сохранять
if (editorRef.value) {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const height = Math.round(entry.contentRect.height)
if (height >= 120) {
saveHeight(height)
}
}
})
resizeObserver.observe(editorRef.value)
}
}) })
onUpdated(refreshIcons) onUpdated(refreshIcons)
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
})
// Экспортируем методы для использования извне // Экспортируем методы для использования извне
defineExpose({ defineExpose({
setContent, setContent,
@@ -263,7 +307,7 @@ defineExpose({
border-radius: 8px; border-radius: 8px;
padding: 14px 16px; padding: 14px 16px;
min-height: 120px; min-height: 120px;
max-height: 400px; max-height: none;
overflow-y: auto; overflow-y: auto;
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;

View File

@@ -67,11 +67,28 @@ const props = defineProps({
const emit = defineEmits(['close', 'update:show']) const emit = defineEmits(['close', 'update:show'])
const STORAGE_KEY = 'taskboard_panel_width'
const panelRef = ref(null) const panelRef = ref(null)
const panelWidth = ref(props.width) const panelWidth = ref(getSavedWidth())
const isResizing = ref(false) const isResizing = ref(false)
const overlayMouseDownTarget = ref(false) const overlayMouseDownTarget = ref(false)
// Получить сохранённую ширину из localStorage
function getSavedWidth() {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
const width = parseInt(saved, 10)
if (!isNaN(width)) return width
}
return props.width
}
// Сохранить ширину в localStorage
function saveWidth(width) {
localStorage.setItem(STORAGE_KEY, String(width))
}
// Resize логика // Resize логика
const startResize = (e) => { const startResize = (e) => {
if (!props.resizable) return if (!props.resizable) return
@@ -89,6 +106,9 @@ const onResize = (e) => {
} }
const stopResize = () => { const stopResize = () => {
// Сохраняем ширину в localStorage
saveWidth(panelWidth.value)
setTimeout(() => { setTimeout(() => {
isResizing.value = false isResizing.value = false
}, 100) }, 100)
@@ -122,10 +142,10 @@ const refreshIcons = () => {
} }
} }
// Сброс ширины при открытии // Восстановление ширины при открытии (из localStorage или дефолтная)
watch(() => props.show, (newVal) => { watch(() => props.show, (newVal) => {
if (newVal) { if (newVal) {
panelWidth.value = props.width panelWidth.value = getSavedWidth()
} }
}) })

View File

@@ -39,31 +39,17 @@
<!-- Список архивных задач --> <!-- Список архивных задач -->
<main class="main"> <main class="main">
<div <div class="archive-list">
class="archive-list"
:class="{ 'drag-over': isDragOver }"
ref="listRef"
@dragover.prevent="handleDragOver"
@dragenter.prevent="handleDragEnter"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<template v-for="(card, index) in filteredCards" :key="card.id">
<!-- Индикатор перед карточкой -->
<div v-if="isDragOver && dropIndex === index" class="drop-indicator"></div>
<ArchiveCard <ArchiveCard
v-for="card in filteredCards"
:key="card.id"
:card="card" :card="card"
:index="index"
:departments="departments" :departments="departments"
:labels="labels" :labels="labels"
@click="openTaskPanel(card)" @click="openTaskPanel(card)"
@restore="handleRestore" @restore="handleRestore"
@delete="confirmDelete" @delete="confirmDelete"
/> />
</template>
<!-- Индикатор в конце списка -->
<div v-if="isDragOver && dropIndex === filteredCards.length" class="drop-indicator"></div>
<!-- Пустое состояние --> <!-- Пустое состояние -->
<div v-if="filteredCards.length === 0 && !loading" class="empty-state"> <div v-if="filteredCards.length === 0 && !loading" class="empty-state">
@@ -137,93 +123,19 @@ const cards = ref([])
const users = ref([]) const users = ref([])
const loading = ref(true) const loading = ref(true)
// Отфильтрованные карточки (сортируем по order) // Отфильтрованные карточки (сортируем по дате завершения, новые сверху)
const filteredCards = computed(() => { const filteredCards = computed(() => {
let result = cards.value let result = cards.value
if (activeDepartment.value) { if (activeDepartment.value) {
result = result.filter(card => card.departmentId === activeDepartment.value) result = result.filter(card => card.departmentId === activeDepartment.value)
} }
return result.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) return result.sort((a, b) => {
}) // Сортировка по дате завершения (новые сверху)
const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0
// Drag & Drop const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0
const listRef = ref(null) return dateB - dateA
const isDragOver = ref(false)
const dropIndex = ref(-1)
let dragEnterCounter = 0
const handleDragEnter = () => {
dragEnterCounter++
isDragOver.value = true
}
const calculateDropIndex = (clientY) => {
if (!listRef.value) return filteredCards.value.length
const cardElements = listRef.value.querySelectorAll('.archive-card')
let index = filteredCards.value.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 = () => {
dragEnterCounter--
if (dragEnterCounter === 0) {
isDragOver.value = false
dropIndex.value = -1
}
}
const handleDrop = async (e) => {
const cardId = parseInt(e.dataTransfer.getData('cardId'))
const fromIndex = parseInt(e.dataTransfer.getData('fromIndex'))
const toIndex = calculateDropIndex(e.clientY)
dragEnterCounter = 0
isDragOver.value = false
dropIndex.value = -1
// Если позиция не изменилась — ничего не делаем
if (fromIndex === toIndex || fromIndex === toIndex - 1) return
// Локально перемещаем карточку
const card = cards.value.find(c => c.id === cardId)
if (!card) return
// Удаляем из старой позиции
const cardIndex = cards.value.indexOf(card)
cards.value.splice(cardIndex, 1)
// Вычисляем новую позицию
let newIndex = toIndex
if (cardIndex < toIndex) newIndex--
// Вставляем в новую позицию
cards.value.splice(newIndex, 0, card)
// Пересчитываем order для всех
cards.value.forEach((c, idx) => {
c.order = idx
}) })
})
// Отправляем на сервер
await cardsApi.updateOrder(cardId, card.columnId, newIndex)
}
// Загрузка данных // Загрузка данных
const fetchData = async () => { const fetchData = async () => {
@@ -462,27 +374,6 @@ onMounted(() => {
min-height: 200px; min-height: 200px;
} }
.archive-list.drag-over {
background: rgba(0, 212, 170, 0.03);
border-radius: 12px;
padding: 8px;
margin: -8px;
}
/* Индикатор места вставки */
.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; }
}
/* Пустое состояние */ /* Пустое состояние */
.empty-state { .empty-state {
display: flex; display: flex;

View File

@@ -11,7 +11,7 @@
Target Server Version : 90200 (9.2.0) Target Server Version : 90200 (9.2.0)
File Encoding : 65001 File Encoding : 65001
Date: 13/01/2026 09:07:01 Date: 14/01/2026 08:21:46
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -45,7 +45,7 @@ CREATE TABLE `accounts_session` (
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Table structure for cards_task -- Table structure for cards_task
@@ -66,8 +66,9 @@ CREATE TABLE `cards_task` (
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`descript` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `descript` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`descript_full` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, `descript_full` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`id_project` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Table structure for columns -- Table structure for columns
@@ -77,6 +78,7 @@ CREATE TABLE `columns` (
`id` int NOT NULL AUTO_INCREMENT, `id` int NOT NULL AUTO_INCREMENT,
`name_columns` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `name_columns` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_project` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
@@ -88,6 +90,7 @@ CREATE TABLE `departments` (
`id` int NOT NULL AUTO_INCREMENT, `id` int NOT NULL AUTO_INCREMENT,
`name_departments` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `name_departments` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_project` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
@@ -103,4 +106,16 @@ CREATE TABLE `labels` (
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for project
-- ----------------------------
DROP TABLE IF EXISTS `project`;
CREATE TABLE `project` (
`id` int NOT NULL AUTO_INCREMENT,
`id_order` int NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_ready` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;