Правки фронта
Дополнительные адаптации для мобилки
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
class="comment"
|
||||
:class="{
|
||||
'comment--own': isOwn,
|
||||
'comment--editing': isEditing,
|
||||
'comment--reply': level > 0
|
||||
}"
|
||||
:style="{ marginLeft: level * 24 + 'px' }"
|
||||
@@ -18,7 +17,7 @@
|
||||
<span class="comment-author">{{ comment.author_name }}</span>
|
||||
<span class="comment-date">{{ formattedDate }}</span>
|
||||
|
||||
<div v-if="!isEditing" class="comment-actions">
|
||||
<div class="comment-actions">
|
||||
<IconButton
|
||||
icon="reply"
|
||||
variant="ghost"
|
||||
@@ -46,56 +45,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Режим редактирования -->
|
||||
<div v-if="isEditing" class="comment-edit-form">
|
||||
<RichTextEditor
|
||||
:modelValue="editText"
|
||||
@update:modelValue="$emit('update:editText', $event)"
|
||||
placeholder="Редактирование комментария..."
|
||||
:show-toolbar="false"
|
||||
ref="editEditor"
|
||||
/>
|
||||
<div class="comment-edit-actions">
|
||||
<button class="btn-comment-cancel" @click="$emit('cancel-edit')">Отмена</button>
|
||||
<button
|
||||
class="btn-comment-save"
|
||||
@click="$emit('save-edit')"
|
||||
:disabled="!editText.trim()"
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Текст комментария -->
|
||||
<div class="comment-text" v-html="comment.text"></div>
|
||||
|
||||
<!-- Обычный вид -->
|
||||
<div v-else>
|
||||
<div class="comment-text" v-html="comment.text"></div>
|
||||
|
||||
<!-- Прикреплённые файлы -->
|
||||
<div v-if="comment.file_img && comment.file_img.length > 0" class="comment-files">
|
||||
<div
|
||||
v-for="(file, index) in comment.file_img"
|
||||
:key="file.name + '-' + index"
|
||||
class="comment-file"
|
||||
@click="!isArchiveFile(file) && openPreview(file)"
|
||||
<!-- Прикреплённые файлы -->
|
||||
<div v-if="comment.file_img && comment.file_img.length > 0" class="comment-files">
|
||||
<div
|
||||
v-for="(file, index) in comment.file_img"
|
||||
:key="file.name + '-' + index"
|
||||
class="comment-file"
|
||||
@click="!isArchiveFile(file) && openPreview(file)"
|
||||
>
|
||||
<template v-if="isArchiveFile(file)">
|
||||
<div class="comment-file-archive">
|
||||
<i data-lucide="archive"></i>
|
||||
<span class="comment-file-ext">.{{ getFileExt(file) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<img v-else :src="getFullUrl(file.url)" :alt="file.name">
|
||||
<a
|
||||
class="comment-file-download"
|
||||
:href="getFullUrl(file.url)"
|
||||
:download="file.name"
|
||||
@click.stop
|
||||
title="Скачать"
|
||||
>
|
||||
<template v-if="isArchiveFile(file)">
|
||||
<div class="comment-file-archive">
|
||||
<i data-lucide="archive"></i>
|
||||
<span class="comment-file-ext">.{{ getFileExt(file) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<img v-else :src="getFullUrl(file.url)" :alt="file.name">
|
||||
<a
|
||||
class="comment-file-download"
|
||||
:href="getFullUrl(file.url)"
|
||||
:download="file.name"
|
||||
@click.stop
|
||||
title="Скачать"
|
||||
>
|
||||
<i data-lucide="download"></i>
|
||||
</a>
|
||||
</div>
|
||||
<i data-lucide="download"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,9 +79,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, onUpdated, watch, nextTick } from 'vue'
|
||||
import { computed, onMounted, onUpdated } from 'vue'
|
||||
import IconButton from '../ui/IconButton.vue'
|
||||
import RichTextEditor from '../ui/RichTextEditor.vue'
|
||||
import { serverSettings } from '../../api'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -125,23 +100,13 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
getFullUrl: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['reply', 'edit', 'delete', 'cancel-edit', 'save-edit', 'update:editText', 'preview-file'])
|
||||
|
||||
const editEditor = ref(null)
|
||||
const emit = defineEmits(['reply', 'edit', 'delete', 'preview-file'])
|
||||
|
||||
const archiveExtensions = ['zip', 'rar']
|
||||
|
||||
@@ -182,14 +147,6 @@ const formattedDate = computed(() => {
|
||||
return `${day} ${months[date.getMonth()]} ${time}`
|
||||
})
|
||||
|
||||
// Установка контента редактора при начале редактирования
|
||||
watch(() => props.isEditing, async (newVal) => {
|
||||
if (newVal) {
|
||||
await nextTick()
|
||||
editEditor.value?.setContent(props.editText)
|
||||
}
|
||||
})
|
||||
|
||||
const refreshIcons = () => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
@@ -198,11 +155,6 @@ const refreshIcons = () => {
|
||||
|
||||
onMounted(refreshIcons)
|
||||
onUpdated(refreshIcons)
|
||||
|
||||
// Expose для установки контента редактора
|
||||
defineExpose({
|
||||
setEditContent: (text) => editEditor.value?.setContent(text)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -227,11 +179,6 @@ defineExpose({
|
||||
background: rgba(0, 212, 170, 0.08);
|
||||
}
|
||||
|
||||
.comment--editing {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
outline: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.comment--reply {
|
||||
border-left: 2px solid rgba(255, 255, 255, 0.1);
|
||||
padding-left: 12px;
|
||||
@@ -322,63 +269,17 @@ defineExpose({
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Mobile: иконки видны всегда */
|
||||
:global(body.is-mobile) .comment-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.comment-btn-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.15) !important;
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
color: var(--red) !important;
|
||||
}
|
||||
|
||||
/* Редактирование комментария */
|
||||
.comment-edit-form {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.comment-edit-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-comment-cancel {
|
||||
padding: 8px 14px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-comment-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-comment-save {
|
||||
padding: 8px 14px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #000;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-comment-save:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-comment-save:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Прикреплённые файлы в комментарии */
|
||||
.comment-files {
|
||||
display: flex;
|
||||
|
||||
@@ -20,15 +20,10 @@
|
||||
:level="comment.level"
|
||||
:is-own="comment.id_accounts === currentUserId"
|
||||
:can-edit="canEditComment(comment)"
|
||||
:is-editing="editingCommentId === comment.id"
|
||||
:edit-text="editingCommentText"
|
||||
:get-full-url="getFullUrl"
|
||||
@update:edit-text="editingCommentText = $event"
|
||||
@reply="startReply"
|
||||
@edit="startEditComment"
|
||||
@delete="confirmDeleteComment"
|
||||
@cancel-edit="cancelEditComment"
|
||||
@save-edit="saveEditComment"
|
||||
@preview-file="(file) => previewFile(file, comment)"
|
||||
/>
|
||||
</div>
|
||||
@@ -53,6 +48,155 @@
|
||||
@confirm="deleteComment"
|
||||
@cancel="showDeleteDialog = false"
|
||||
/>
|
||||
|
||||
<!-- Панель редактирования комментария -->
|
||||
<SlidePanel
|
||||
:show="editPanelOpen"
|
||||
:width="500"
|
||||
:min-width="400"
|
||||
:max-width="700"
|
||||
@close="cancelEditPanel"
|
||||
>
|
||||
<template #header>
|
||||
<div class="edit-panel-header">
|
||||
<div v-if="editingComment" class="edit-author-avatar">
|
||||
<img v-if="editingComment.author_avatar" :src="getFullUrl(editingComment.author_avatar)" alt="">
|
||||
<i v-else data-lucide="user"></i>
|
||||
</div>
|
||||
<div class="edit-header-text">
|
||||
<h2>Редактирование</h2>
|
||||
<span v-if="editingComment" class="edit-author-name">{{ editingComment.author_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="edit-panel-content">
|
||||
<!-- Редактор текста -->
|
||||
<div class="edit-form-section">
|
||||
<div class="edit-form-label">
|
||||
<span>Текст комментария</span>
|
||||
<div class="format-buttons">
|
||||
<button type="button" class="format-btn" @mousedown.prevent="applyEditFormat('bold')" title="Жирный">
|
||||
<i data-lucide="bold"></i>
|
||||
</button>
|
||||
<button type="button" class="format-btn" @mousedown.prevent="applyEditFormat('italic')" title="Курсив">
|
||||
<i data-lucide="italic"></i>
|
||||
</button>
|
||||
<button type="button" class="format-btn" @mousedown.prevent="applyEditFormat('underline')" title="Подчёркивание">
|
||||
<i data-lucide="underline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RichTextEditor
|
||||
:modelValue="editingCommentText"
|
||||
@update:modelValue="editingCommentText = $event"
|
||||
placeholder="Текст комментария..."
|
||||
:show-toolbar="false"
|
||||
ref="editEditorRef"
|
||||
class="edit-editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Существующие файлы -->
|
||||
<div v-if="editExistingFiles.length > 0" class="edit-form-section">
|
||||
<div class="edit-form-label">
|
||||
<span>Прикреплённые файлы</span>
|
||||
</div>
|
||||
<div class="edit-files-list">
|
||||
<div
|
||||
v-for="(file, index) in editExistingFiles"
|
||||
:key="'existing-' + file.name + '-' + index"
|
||||
class="edit-file-item"
|
||||
:class="{ 'file-deleted': filesToDelete.includes(file.name) }"
|
||||
>
|
||||
<div class="edit-file-preview">
|
||||
<img v-if="isImageFile(file)" :src="getFullUrl(file.url)" :alt="file.name">
|
||||
<div v-else class="edit-file-icon">
|
||||
<i data-lucide="archive"></i>
|
||||
<span>.{{ getFileExt(file) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="edit-file-name" :title="file.name">{{ file.name }}</span>
|
||||
<button
|
||||
v-if="!filesToDelete.includes(file.name)"
|
||||
class="edit-file-delete"
|
||||
@click="markFileForDelete(file.name)"
|
||||
title="Удалить файл"
|
||||
>
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="edit-file-restore"
|
||||
@click="unmarkFileForDelete(file.name)"
|
||||
title="Восстановить файл"
|
||||
>
|
||||
<i data-lucide="undo-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Новые файлы -->
|
||||
<div class="edit-form-section">
|
||||
<div class="edit-form-label">
|
||||
<span>Добавить файлы</span>
|
||||
</div>
|
||||
<div v-if="editNewFiles.length > 0" class="edit-files-list">
|
||||
<div
|
||||
v-for="(file, index) in editNewFiles"
|
||||
:key="'new-' + file.name + '-' + index"
|
||||
class="edit-file-item new-file"
|
||||
>
|
||||
<div class="edit-file-preview">
|
||||
<img v-if="file.preview" :src="file.preview" :alt="file.name">
|
||||
<div v-else class="edit-file-icon">
|
||||
<i data-lucide="archive"></i>
|
||||
<span>.{{ getFileExtFromName(file.name) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="edit-file-name" :title="file.name">{{ file.name }}</span>
|
||||
<button
|
||||
class="edit-file-delete"
|
||||
@click="removeNewFile(index)"
|
||||
title="Удалить"
|
||||
>
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-add-file" @click="triggerEditFileInput">
|
||||
<i data-lucide="paperclip"></i>
|
||||
Прикрепить файл
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
ref="editFileInputRef"
|
||||
accept=".png,.jpg,.jpeg,.zip,.rar,image/png,image/jpeg,application/zip,application/x-rar-compressed"
|
||||
multiple
|
||||
@change="handleEditFileSelect"
|
||||
style="display: none"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-cancel" @click="cancelEditPanel">
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
class="btn-save-edit"
|
||||
@click="saveEditPanel"
|
||||
:disabled="!editingCommentText.trim() || isSavingEdit"
|
||||
>
|
||||
<span v-if="isSavingEdit" class="btn-loader"></span>
|
||||
<template v-else>
|
||||
<i data-lucide="check"></i>
|
||||
Сохранить
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</SlidePanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,7 +205,12 @@ import { ref, computed, watch, onMounted, onUpdated, onUnmounted, nextTick } fro
|
||||
import CommentItem from './CommentItem.vue'
|
||||
import CommentForm from './CommentForm.vue'
|
||||
import ConfirmDialog from '../ConfirmDialog.vue'
|
||||
import SlidePanel from '../ui/SlidePanel.vue'
|
||||
import RichTextEditor from '../ui/RichTextEditor.vue'
|
||||
import { commentsApi, commentImageApi, getFullUrl } from '../../api'
|
||||
import { useMobile } from '../../composables/useMobile'
|
||||
|
||||
const { isMobile } = useMobile()
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
@@ -266,34 +415,166 @@ const canEditComment = (comment) => {
|
||||
return comment.id_accounts === props.currentUserId || props.isProjectAdmin
|
||||
}
|
||||
|
||||
// Edit panel state
|
||||
const editPanelOpen = ref(false)
|
||||
const editEditorRef = ref(null)
|
||||
const editingComment = ref(null)
|
||||
const isSavingEdit = ref(false)
|
||||
|
||||
// Files management
|
||||
const editExistingFiles = ref([])
|
||||
const editNewFiles = ref([])
|
||||
const filesToDelete = ref([])
|
||||
const editFileInputRef = ref(null)
|
||||
|
||||
const allowedExtensions = ['png', 'jpg', 'jpeg', 'zip', 'rar']
|
||||
const archiveExtensions = ['zip', 'rar']
|
||||
const maxFileSize = 10 * 1024 * 1024 // 10 MB
|
||||
|
||||
const getFileExt = (file) => file.name?.split('.').pop()?.toLowerCase() || ''
|
||||
const getFileExtFromName = (name) => name?.split('.').pop()?.toLowerCase() || ''
|
||||
const isImageFile = (file) => !archiveExtensions.includes(getFileExt(file))
|
||||
const isAllowedFile = (file) => allowedExtensions.includes(getFileExtFromName(file.name))
|
||||
|
||||
const startEditComment = (comment) => {
|
||||
editingCommentId.value = comment.id
|
||||
editingCommentText.value = comment.text
|
||||
editingComment.value = comment
|
||||
editExistingFiles.value = [...(comment.file_img || [])]
|
||||
editNewFiles.value = []
|
||||
filesToDelete.value = []
|
||||
|
||||
editPanelOpen.value = true
|
||||
nextTick(() => {
|
||||
editEditorRef.value?.setContent(comment.text)
|
||||
refreshIcons()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelEditComment = () => {
|
||||
editingCommentId.value = null
|
||||
editingCommentText.value = ''
|
||||
editingComment.value = null
|
||||
editExistingFiles.value = []
|
||||
editNewFiles.value = []
|
||||
filesToDelete.value = []
|
||||
}
|
||||
|
||||
const saveEditComment = async () => {
|
||||
if (!editingCommentText.value.trim() || !editingCommentId.value) return
|
||||
|
||||
try {
|
||||
const result = await commentsApi.update(editingCommentId.value, editingCommentText.value)
|
||||
if (result.success) {
|
||||
const index = comments.value.findIndex(c => c.id === editingCommentId.value)
|
||||
if (index !== -1) {
|
||||
comments.value[index] = result.comment
|
||||
}
|
||||
cancelEditComment()
|
||||
const cancelEditPanel = () => {
|
||||
editPanelOpen.value = false
|
||||
cancelEditComment()
|
||||
}
|
||||
|
||||
const applyEditFormat = (command) => {
|
||||
editEditorRef.value?.applyFormat(command)
|
||||
}
|
||||
|
||||
// File management
|
||||
const markFileForDelete = (fileName) => {
|
||||
filesToDelete.value.push(fileName)
|
||||
}
|
||||
|
||||
const unmarkFileForDelete = (fileName) => {
|
||||
filesToDelete.value = filesToDelete.value.filter(f => f !== fileName)
|
||||
}
|
||||
|
||||
const triggerEditFileInput = () => {
|
||||
editFileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const handleEditFileSelect = (event) => {
|
||||
const selectedFiles = event.target.files
|
||||
if (selectedFiles) {
|
||||
processEditFiles(Array.from(selectedFiles))
|
||||
}
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const processEditFiles = (fileList) => {
|
||||
for (const file of fileList) {
|
||||
if (!isAllowedFile(file)) {
|
||||
console.warn(`Файл "${file.name}" не поддерживается.`)
|
||||
continue
|
||||
}
|
||||
if (file.size > maxFileSize) {
|
||||
console.warn(`Файл "${file.name}" слишком большой.`)
|
||||
continue
|
||||
}
|
||||
const isDuplicate = editNewFiles.value.some(f => f.name === file.name && f.size === file.size)
|
||||
if (isDuplicate) continue
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const isImage = ['png', 'jpg', 'jpeg'].includes(getFileExtFromName(file.name))
|
||||
editNewFiles.value.push({
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
data: e.target.result,
|
||||
preview: isImage ? e.target.result : null
|
||||
})
|
||||
refreshIcons()
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
const removeNewFile = (index) => {
|
||||
editNewFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const saveEditPanel = async () => {
|
||||
if (!editingCommentText.value.trim() || !editingCommentId.value) return
|
||||
|
||||
isSavingEdit.value = true
|
||||
try {
|
||||
// 1. Обновляем текст комментария
|
||||
const result = await commentsApi.update(editingCommentId.value, editingCommentText.value)
|
||||
if (!result.success) throw new Error('Ошибка обновления текста')
|
||||
|
||||
// 2. Удаляем отмеченные файлы
|
||||
if (filesToDelete.value.length > 0) {
|
||||
await commentImageApi.delete(editingCommentId.value, filesToDelete.value)
|
||||
}
|
||||
|
||||
// 3. Загружаем новые файлы
|
||||
const uploadedFiles = []
|
||||
for (const file of editNewFiles.value) {
|
||||
try {
|
||||
const uploadResult = await commentImageApi.upload(editingCommentId.value, file.data, file.name)
|
||||
if (uploadResult.success) {
|
||||
uploadedFiles.push(uploadResult.file)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки файла:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Обновляем комментарий в списке
|
||||
const index = comments.value.findIndex(c => c.id === editingCommentId.value)
|
||||
if (index !== -1) {
|
||||
const updatedComment = result.comment
|
||||
// Обновляем file_img: убираем удалённые, добавляем новые
|
||||
const remainingFiles = (editExistingFiles.value || []).filter(f => !filesToDelete.value.includes(f.name))
|
||||
updatedComment.file_img = [...remainingFiles, ...uploadedFiles]
|
||||
comments.value[index] = updatedComment
|
||||
}
|
||||
|
||||
editPanelOpen.value = false
|
||||
cancelEditComment()
|
||||
refreshIcons()
|
||||
} catch (e) {
|
||||
console.error('Ошибка обновления комментария:', e)
|
||||
} finally {
|
||||
isSavingEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop inline edit (deprecated, kept for compatibility)
|
||||
const saveEditComment = async () => {
|
||||
await saveEditPanel()
|
||||
}
|
||||
|
||||
// Удаление
|
||||
const confirmDeleteComment = (comment) => {
|
||||
commentToDelete.value = comment
|
||||
@@ -419,12 +700,10 @@ defineExpose({
|
||||
}
|
||||
|
||||
/* Mobile: убираем max-height, используем flex */
|
||||
@media (max-width: 768px) {
|
||||
.comments-list {
|
||||
max-height: none;
|
||||
min-height: 50px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
:global(body.is-mobile) .comments-list {
|
||||
max-height: none;
|
||||
min-height: 50px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.comments-loading,
|
||||
@@ -459,4 +738,314 @@ defineExpose({
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Edit Panel Styles ========== */
|
||||
.edit-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.edit-panel-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edit-author-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-author-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.edit-author-avatar i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.edit-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.edit-author-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.edit-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.edit-form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.edit-form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.format-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.format-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
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: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.edit-editor {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.edit-editor :deep(.editor-content) {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* Files list */
|
||||
.edit-files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.edit-file-item.new-file {
|
||||
background: rgba(0, 212, 170, 0.05);
|
||||
border-color: rgba(0, 212, 170, 0.2);
|
||||
}
|
||||
|
||||
.edit-file-item.file-deleted {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.edit-file-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.edit-file-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.edit-file-icon i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.edit-file-icon span {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.edit-file-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-file-delete,
|
||||
.edit-file-restore {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-file-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.edit-file-restore:hover {
|
||||
background: rgba(0, 212, 170, 0.15);
|
||||
border-color: rgba(0, 212, 170, 0.3);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.edit-file-delete i,
|
||||
.edit-file-restore i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-add-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-add-file:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-add-file i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Footer buttons */
|
||||
.btn-cancel {
|
||||
padding: 12px 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-save-edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #000;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-save-edit:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-save-edit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-save-edit i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.btn-loader {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
border-top-color: #000;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user