Добавление в Архив + Фронт
1. Переписал модуль выпадающего слева меню 2. Добавил механику Архивации задач 3. Запоминания выбранного отдела
This commit is contained in:
282
front_vue/src/components/ui/SlidePanel.vue
Normal file
282
front_vue/src/components/ui/SlidePanel.vue
Normal 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>
|
||||
Reference in New Issue
Block a user