1
0

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

1. Добавление тегов
2. Новые алгоритмы
This commit is contained in:
2026-01-12 09:14:41 +07:00
parent 35d7e0ec8b
commit 1b4c66a7b1
3 changed files with 268 additions and 8 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ front_vue/dist/
# Публичные файлы backend # Публичные файлы backend
backend/public/* backend/public/*
# Личные файлы
deploy.js
deploy.php

View File

@@ -44,7 +44,7 @@
<span v-if="card.dateCreate" class="date-create"> <span v-if="card.dateCreate" class="date-create">
Создано: {{ formatDateWithYear(card.dateCreate) }} Создано: {{ formatDateWithYear(card.dateCreate) }}
</span> </span>
<span v-if="card.dueDate" class="due-date" :class="dueDateStatus"> <span v-if="card.dueDate && Number(columnId) !== 4" class="due-date" :class="dueDateStatus">
{{ daysLeftText }} {{ daysLeftText }}
</span> </span>
</div> </div>

View File

@@ -41,12 +41,32 @@
</div> </div>
<div class="field"> <div class="field">
<div class="field-header">
<label>Подробное описание</label> <label>Подробное описание</label>
<textarea <div class="format-buttons">
v-model="form.details" <button type="button" class="format-btn" @mousedown.prevent="applyFormat('bold')" title="Жирный (Ctrl+B)">
placeholder="Подробное описание задачи, заметки, ссылки..." <i data-lucide="bold"></i>
rows="8" </button>
></textarea> <button type="button" class="format-btn" @mousedown.prevent="applyFormat('italic')" title="Курсив (Ctrl+I)">
<i data-lucide="italic"></i>
</button>
<button type="button" class="format-btn" @mousedown.prevent="applyFormat('underline')" title="Подчёркивание (Ctrl+U)">
<i data-lucide="underline"></i>
</button>
</div>
</div>
<!-- Редактируемое поле с форматированием -->
<div
class="details-editor"
:class="{ 'is-empty': !form.details }"
contenteditable="true"
ref="detailsInput"
@input="onDetailsInput"
@paste="onDetailsPaste"
@keydown="onDetailsKeydown"
@click="onDetailsClick"
:data-placeholder="'Подробное описание задачи, заметки, ссылки...'"
></div>
</div> </div>
<div class="field"> <div class="field">
@@ -340,6 +360,7 @@ const dropdownRef = ref(null)
const searchInput = ref(null) const searchInput = ref(null)
const panelRef = ref(null) const panelRef = ref(null)
const fileInput = ref(null) const fileInput = ref(null)
const detailsInput = ref(null)
const dropdownOpen = ref(false) const dropdownOpen = ref(false)
const userSearch = ref('') const userSearch = ref('')
@@ -591,6 +612,9 @@ watch(() => props.show, async (newVal) => {
await nextTick() await nextTick()
saveInitialForm() saveInitialForm()
// Устанавливаем содержимое редактора описания
setEditorContent(form.details)
await nextTick() await nextTick()
titleInput.value?.focus() titleInput.value?.focus()
refreshIcons() refreshIcons()
@@ -831,6 +855,129 @@ const refreshIcons = () => {
} }
} }
// Применить форматирование (toggle)
const applyFormat = (command) => {
detailsInput.value?.focus()
document.execCommand(command, false, null)
syncDetailsFromEditor()
}
// Синхронизация содержимого редактора с form.details
const syncDetailsFromEditor = () => {
if (!detailsInput.value) return
// Нормализуем переносы строк в <br>
let html = detailsInput.value.innerHTML
html = html.replace(/<\/div><div>/gi, '<br>')
html = html.replace(/<div>/gi, '')
html = html.replace(/<\/div>/gi, '')
form.details = html
}
// Обработка ввода
const onDetailsInput = () => {
syncDetailsFromEditor()
}
// Обработка клика (открытие ссылок)
const onDetailsClick = (e) => {
// Если кликнули по ссылке — открываем в новой вкладке
if (e.target.tagName === 'A') {
e.preventDefault()
window.open(e.target.href, '_blank')
}
}
// Обработка вставки (очистка форматирования из внешних источников)
const onDetailsPaste = (e) => {
e.preventDefault()
const text = e.clipboardData.getData('text/plain')
document.execCommand('insertText', false, text)
// Подсвечиваем ссылки после вставки
setTimeout(linkifyContent, 0)
}
// Горячие клавиши
const onDetailsKeydown = (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'b') {
e.preventDefault()
applyFormat('bold')
} else if (e.key === 'i') {
e.preventDefault()
applyFormat('italic')
} else if (e.key === 'u') {
e.preventDefault()
applyFormat('underline')
}
}
// После пробела или Enter — подсвечиваем ссылки
if (e.key === ' ' || e.key === 'Enter') {
setTimeout(linkifyContent, 0)
}
}
// Автоматическое преобразование URL в ссылки (вызывается после пробела/Enter)
const linkifyContent = () => {
if (!detailsInput.value) return
const html = detailsInput.value.innerHTML
// Ищем URL которые ещё не в тегах <a>
// Разбиваем HTML на части: теги <a>...</a> и остальной текст
const parts = html.split(/(<a [^>]*>.*?<\/a>)/gi)
let changed = false
const newParts = parts.map(part => {
// Если это уже ссылка — не трогаем
if (part.startsWith('<a ')) return part
// Ищем URL в этой части
const newPart = part.replace(
/(https?:\/\/[^\s<>"]+)/g,
'<a href="$1" target="_blank" rel="noopener">$1</a>'
)
if (newPart !== part) changed = true
return newPart
})
if (changed) {
// Сохраняем позицию курсора (в конце)
detailsInput.value.innerHTML = newParts.join('')
// Ставим курсор в конец
const range = document.createRange()
const selection = window.getSelection()
range.selectNodeContents(detailsInput.value)
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
syncDetailsFromEditor()
}
}
// Установка содержимого редактора
const setEditorContent = (html) => {
if (detailsInput.value) {
detailsInput.value.innerHTML = linkifyHtml(html || '')
}
}
// Простое преобразование URL в ссылки (без сложных regex)
const linkifyHtml = (html) => {
if (!html) return ''
// Если уже есть теги <a> — не трогаем
if (html.includes('<a ')) return html
// Простая замена URL на ссылки
return html.replace(
/(https?:\/\/[^\s<>"]+)/g,
'<a href="$1" target="_blank" rel="noopener">$1</a>'
)
}
onMounted(() => { onMounted(() => {
refreshIcons() refreshIcons()
document.addEventListener('click', handleClickOutside) document.addEventListener('click', handleClickOutside)
@@ -967,6 +1114,48 @@ onUpdated(refreshIcons)
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.field-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.format-buttons {
display: flex;
gap: 3px;
}
.format-btn {
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 5px;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.format-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.15);
color: var(--text-primary);
}
.format-btn:active {
background: var(--accent);
border-color: var(--accent);
color: #000;
}
.format-btn i {
width: 10px;
height: 10px;
}
.field input { .field input {
background: rgba(255, 255, 255, 0.04); background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.08);
@@ -1009,6 +1198,73 @@ onUpdated(refreshIcons)
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
} }
/* Редактор описания с форматированием */
.details-editor {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 14px 16px;
min-height: 120px;
max-height: 400px;
overflow-y: auto;
color: var(--text-primary);
font-size: 14px;
line-height: 1.6;
transition: border-color 0.15s, background 0.15s;
word-break: break-word;
outline: none;
resize: vertical;
}
.details-editor:focus {
border-color: var(--accent);
background: rgba(255, 255, 255, 0.06);
}
.details-editor:empty::before {
content: attr(data-placeholder);
color: var(--text-muted);
pointer-events: none;
}
.details-editor::-webkit-scrollbar {
width: 6px;
}
.details-editor::-webkit-scrollbar-track {
background: transparent;
}
.details-editor::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
}
.details-editor :deep(a) {
color: var(--accent);
text-decoration: none;
cursor: pointer;
}
.details-editor :deep(a:hover) {
text-decoration: underline;
cursor: pointer;
}
.details-editor :deep(b),
.details-editor :deep(strong) {
font-weight: 600;
}
.details-editor :deep(i),
.details-editor :deep(em) {
font-style: italic;
}
.details-editor :deep(u) {
text-decoration: underline;
}
.field textarea::-webkit-scrollbar { .field textarea::-webkit-scrollbar {
width: 6px; width: 6px;
} }