1
0

Добавление в Архив + Фронт

1. Переписал модуль выпадающего слева меню
2. Добавил механику Архивации задач
3. Запоминания выбранного отдела
This commit is contained in:
2026-01-13 07:04:10 +07:00
parent 6688b8e37c
commit 44b6e636d4
17 changed files with 2434 additions and 1594 deletions

View File

@@ -0,0 +1,282 @@
<template>
<Transition name="panel">
<div
v-if="show"
class="panel-overlay"
@mousedown.self="onOverlayMouseDown"
@mouseup.self="onOverlayMouseUp"
>
<div
class="panel"
:style="{ width: panelWidth + 'px' }"
ref="panelRef"
@mousedown="overlayMouseDownTarget = false"
>
<!-- Ручка для изменения ширины -->
<div
class="resize-handle"
@mousedown="startResize"
></div>
<div class="panel-header">
<div class="header-content">
<slot name="header"></slot>
</div>
<button class="btn-close" @click="handleClose">
<i data-lucide="x"></i>
</button>
</div>
<div class="panel-body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="panel-footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
import { ref, onMounted, onUnmounted, onUpdated, watch } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
width: {
type: Number,
default: 600
},
minWidth: {
type: Number,
default: 500
},
maxWidth: {
type: Number,
default: 1000
},
resizable: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['close', 'update:show'])
const panelRef = ref(null)
const panelWidth = ref(props.width)
const isResizing = ref(false)
const overlayMouseDownTarget = ref(false)
// Resize логика
const startResize = (e) => {
if (!props.resizable) return
isResizing.value = true
document.addEventListener('mousemove', onResize)
document.addEventListener('mouseup', stopResize)
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'
}
const onResize = (e) => {
if (!isResizing.value) return
const newWidth = window.innerWidth - e.clientX
panelWidth.value = Math.min(props.maxWidth, Math.max(props.minWidth, newWidth))
}
const stopResize = () => {
setTimeout(() => {
isResizing.value = false
}, 100)
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
// Закрытие по клику на overlay
const onOverlayMouseDown = () => {
overlayMouseDownTarget.value = true
}
const onOverlayMouseUp = () => {
if (overlayMouseDownTarget.value && !isResizing.value) {
handleClose()
}
overlayMouseDownTarget.value = false
}
const handleClose = () => {
emit('close')
emit('update:show', false)
}
// Обновление иконок Lucide
const refreshIcons = () => {
if (window.lucide) {
window.lucide.createIcons()
}
}
// Сброс ширины при открытии
watch(() => props.show, (newVal) => {
if (newVal) {
panelWidth.value = props.width
}
})
onMounted(refreshIcons)
onUpdated(refreshIcons)
onUnmounted(() => {
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
})
</script>
<style scoped>
.panel-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.panel {
position: fixed;
top: 0;
right: 0;
max-width: 100%;
height: 100vh;
background: #18181b;
display: flex;
flex-direction: column;
box-shadow: -10px 0 40px rgba(0, 0, 0, 0.4);
}
.resize-handle {
position: absolute;
left: 0;
top: 0;
width: 6px;
height: 100%;
cursor: ew-resize;
background: transparent;
transition: background 0.15s;
z-index: 10;
}
.resize-handle:hover,
.resize-handle:active {
background: var(--accent);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.header-content {
display: flex;
align-items: baseline;
gap: 16px;
flex: 1;
}
.header-content :deep(h2) {
font-size: 20px;
font-weight: 500;
margin: 0;
}
.header-content :deep(.header-date) {
font-size: 13px;
color: var(--text-muted);
}
.btn-close {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.15s;
flex-shrink: 0;
}
.btn-close i {
width: 20px;
height: 20px;
}
.btn-close:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-primary);
}
.panel-body {
flex: 1;
padding: 32px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.panel-body::-webkit-scrollbar {
width: 6px;
}
.panel-body::-webkit-scrollbar-track {
background: transparent;
}
.panel-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
}
.panel-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
.panel-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
/* Transition */
.panel-enter-active,
.panel-leave-active {
transition: all 0.3s ease;
}
.panel-enter-active .panel,
.panel-leave-active .panel {
transition: transform 0.3s ease;
}
.panel-enter-from,
.panel-leave-to {
opacity: 0;
}
.panel-enter-from .panel,
.panel-leave-to .panel {
transform: translateX(100%);
}
</style>