changes
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
<div
|
||||
v-for="(block, index) in blocks"
|
||||
:key="block._uid"
|
||||
class="group relative bg-white border border-layer-line rounded-lg overflow-hidden"
|
||||
class="group relative bg-white border border-layer-line rounded-lg"
|
||||
>
|
||||
<!-- Block Header -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 bg-muted/30 border-b border-layer-line">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3" style="overflow: visible;">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -12,7 +12,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="overflow: visible;">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Текст <span class="text-danger">*</span>
|
||||
</label>
|
||||
@@ -90,9 +90,9 @@ export default {
|
||||
tinymce.init({
|
||||
selector: `#${this.editorId}`,
|
||||
license_key: 'gpl',
|
||||
autoresize_bottom_margin: 20,
|
||||
autoresize_overflow_padding: 20,
|
||||
max_height: 600,
|
||||
min_height: 400,
|
||||
autoresize_bottom_margin: 30,
|
||||
autoresize_overflow_padding: 30,
|
||||
menubar: false,
|
||||
statusbar: true,
|
||||
branding: false,
|
||||
@@ -105,14 +105,13 @@ export default {
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
'removeformat | help | fullscreen',
|
||||
content_style: 'body { font-family: Inter, -apple-system, sans-serif; font-size: 14px; }',
|
||||
setup: (editor) => {
|
||||
this.editor = editor;
|
||||
editor.on('init', () => {
|
||||
editor.setContent(this.modelValue.content || '');
|
||||
this.loading = false;
|
||||
// Emit initial content to ensure parent has correct value
|
||||
this.update('content', editor.getContent());
|
||||
});
|
||||
editor.on('change', () => {
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
<DashboardIcon v-if="tab.id === 'main'" name="information-circle" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||
<DashboardIcon v-if="tab.id === 'content'" name="document-text" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||
<DashboardIcon v-if="tab.id === 'media'" name="photo" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||
<DashboardIcon v-if="tab.id === 'slider'" name="presentation-chart-bar" size="4" :class="activeTab === tab.id ? 'text-primary' : 'text-muted-foreground-2'" />
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -305,7 +304,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<!-- Social Media — ВРЕМЕННО СКРЫТО -->
|
||||
<!--
|
||||
<div class="border border-layer-line rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 bg-surface/50 border-b border-line-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -325,9 +325,12 @@
|
||||
Опубликовать ВКонтакте
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted-foreground-1">Новость будет автоматически опубликована в VK</p>
|
||||
<p class="mt-2 text-xs text-muted-foreground-1">
|
||||
{{ isEdit ? 'Новость будет обновлена в VK' : 'Новость будет автоматически опубликована в VK' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<!-- Tab: Content -->
|
||||
@@ -473,14 +476,19 @@
|
||||
<div class="space-y-3 text-center">
|
||||
<div class="flex justify-center">
|
||||
<div :class="[isDraggingGallery ? 'bg-primary/20' : 'bg-primary/10', 'w-12 h-12 rounded-lg flex items-center justify-center transition-all']">
|
||||
<DashboardIcon name="cloud-arrow-up" size="6" class="text-primary" />
|
||||
<svg v-if="uploadingImages" class="animate-spin w-6 h-6 text-primary" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<DashboardIcon v-else name="cloud-arrow-up" size="6" class="text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
<span class="text-primary">Нажмите для выбора</span> или перетащите файлы
|
||||
<span v-if="uploadingImages">Загрузка...</span>
|
||||
<span v-else><span class="text-primary">Нажмите для выбора</span> или перетащите файлы</span>
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground-1 mt-1">JPG, PNG (до 20MB каждый, макс. 200 файлов)</p>
|
||||
<p class="text-xs text-muted-foreground-1 mt-1">JPG, PNG, WebP (до 20MB каждый, макс. 200 файлов)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -546,14 +554,6 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Slider -->
|
||||
<div v-if="activeTab === 'slider'" class="space-y-6">
|
||||
<div class="text-center py-12">
|
||||
<DashboardIcon name="information-circle" size="16" class="text-gray-400 mx-auto mb-4" />
|
||||
<p class="text-sm text-gray-600">Управление слайдами доступно в разделе "Слайдеры" Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -582,10 +582,6 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
sliders: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
statuses: {
|
||||
type: Array,
|
||||
required: true
|
||||
@@ -596,10 +592,10 @@ export default {
|
||||
return {
|
||||
activeTab: 'main',
|
||||
isEdit: false,
|
||||
isSliderEnabled: false,
|
||||
activeButton: true,
|
||||
isDraggingPreview: false,
|
||||
isDraggingGallery: false,
|
||||
uploadingImages: false,
|
||||
newTag: '',
|
||||
newAuthor: '',
|
||||
PostStatus,
|
||||
@@ -642,8 +638,7 @@ export default {
|
||||
tabs: [
|
||||
{ id: 'main', label: 'Основная информация' },
|
||||
{ id: 'content', label: 'Содержание' },
|
||||
{ id: 'media', label: 'Медиа' },
|
||||
{ id: 'slider', label: 'Слайдер' }
|
||||
{ id: 'media', label: 'Медиа' }
|
||||
],
|
||||
slugGenerated: false
|
||||
}
|
||||
@@ -685,25 +680,24 @@ export default {
|
||||
if (typeof this.form.preview === 'string') {
|
||||
return `/storage/${this.form.preview}`;
|
||||
}
|
||||
return URL.createObjectURL(this.form.preview);
|
||||
if (this.form.preview instanceof File || this.form.preview instanceof Blob) {
|
||||
return URL.createObjectURL(this.form.preview);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
galleryUrls() {
|
||||
return this.form.images.map(img => {
|
||||
if (!img) return null;
|
||||
if (typeof img === 'string') {
|
||||
return `/storage/${img}`;
|
||||
}
|
||||
return URL.createObjectURL(img);
|
||||
});
|
||||
if (img instanceof File || img instanceof Blob) {
|
||||
return URL.createObjectURL(img);
|
||||
}
|
||||
return null;
|
||||
}).filter(url => url !== null);
|
||||
},
|
||||
|
||||
slideImageUrl() {
|
||||
if (!this.form.slide.image.url) return null;
|
||||
if (typeof this.form.slide.image.url === 'string') {
|
||||
return `/storage/${this.form.slide.image.url}`;
|
||||
}
|
||||
return URL.createObjectURL(this.form.slide.image.url);
|
||||
}
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
@@ -729,6 +723,7 @@ export default {
|
||||
|
||||
initializeForm() {
|
||||
const p = this.post;
|
||||
|
||||
this.form = {
|
||||
...this.form,
|
||||
title: p.title || '',
|
||||
@@ -739,13 +734,11 @@ export default {
|
||||
authors: p.authors || [],
|
||||
content: p.content || [],
|
||||
preview: p.preview || null,
|
||||
images: p.images || [],
|
||||
images: Array.isArray(p.images) ? [...p.images] : [],
|
||||
publish_setting: p.publish_setting || { publish_after: false, publish_at: null },
|
||||
publication: p.publication || { vk: true, telegram: true },
|
||||
slide: p.slide || this.form.slide
|
||||
};
|
||||
|
||||
this.isSliderEnabled = !!(p.slide && p.slide.slider_id);
|
||||
},
|
||||
|
||||
inputClass(field) {
|
||||
@@ -793,14 +786,44 @@ export default {
|
||||
handlePreviewDrop(e) {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0 && this.isImageFile(files[0])) {
|
||||
this.form.preview = files[0];
|
||||
this.uploadPreviewToServer(files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
handlePreviewFileSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file && this.isImageFile(file)) {
|
||||
this.form.preview = file;
|
||||
this.uploadPreviewToServer(file);
|
||||
}
|
||||
},
|
||||
|
||||
async uploadPreviewToServer(file) {
|
||||
if (!file) return;
|
||||
|
||||
this.uploadingImages = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('images[]', file);
|
||||
|
||||
const response = await fetch(route('dashboard.posts.upload-images'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.paths && result.paths.length > 0) {
|
||||
this.form.preview = result.paths[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Preview upload error:', error);
|
||||
} finally {
|
||||
this.uploadingImages = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -810,16 +833,48 @@ export default {
|
||||
|
||||
handleGalleryDrop(e) {
|
||||
const files = Array.from(e.dataTransfer.files).filter(f => this.isImageFile(f));
|
||||
files.forEach(file => {
|
||||
this.form.images.push(file);
|
||||
});
|
||||
this.uploadImagesToServer(files);
|
||||
},
|
||||
|
||||
handleGalleryFileSelect(e) {
|
||||
const files = Array.from(e.target.files).filter(f => this.isImageFile(f));
|
||||
files.forEach(file => {
|
||||
this.form.images.push(file);
|
||||
});
|
||||
this.uploadImagesToServer(files);
|
||||
// Сбрасываем input чтобы можно было выбрать те же файлы снова
|
||||
e.target.value = '';
|
||||
},
|
||||
|
||||
async uploadImagesToServer(files) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
this.uploadingImages = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
files.forEach(file => {
|
||||
formData.append('images[]', file);
|
||||
});
|
||||
|
||||
const response = await fetch(route('dashboard.posts.upload-images'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.paths) {
|
||||
result.paths.forEach(path => {
|
||||
this.form.images.push(path);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error);
|
||||
} finally {
|
||||
this.uploadingImages = false;
|
||||
}
|
||||
},
|
||||
|
||||
triggerGalleryInput() {
|
||||
@@ -884,27 +939,36 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
handleSlideImageSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file && this.isImageFile(file)) {
|
||||
this.form.slide.image.url = file;
|
||||
}
|
||||
},
|
||||
|
||||
isImageFile(file) {
|
||||
return ['image/jpeg', 'image/png', 'image/webp'].includes(file.type);
|
||||
},
|
||||
|
||||
submitForm() {
|
||||
this.form.processing = true;
|
||||
this.form.errors = {};
|
||||
|
||||
// Фильтруем изображения - оставляем только строки (пути)
|
||||
this.form.images = this.form.images.filter(img => typeof img === 'string' && img.length > 0);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
Object.keys(this.form).forEach(key => {
|
||||
if (key === 'errors' || key === 'processing') return;
|
||||
|
||||
const value = this.form[key];
|
||||
let value = this.form[key];
|
||||
|
||||
// Пропускаем preview если null, пустая строка или пустой объект
|
||||
if (key === 'preview') {
|
||||
if (!value || value === '' || (typeof value === 'object' && Object.keys(value).length === 0)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Пропускаем images если массив пустой
|
||||
if (key === 'images' && Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
// Пропускаем category_id если null
|
||||
if (key === 'category_id' && (value === null || value === '')) return;
|
||||
|
||||
// Пропускаем slide если slider_id null и нет данных
|
||||
if (key === 'slide' && value && !value.slider_id && !value.title && !value.content) return;
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
} else {
|
||||
@@ -916,15 +980,13 @@ export default {
|
||||
? route('dashboard.posts.update', this.post.id)
|
||||
: route('dashboard.posts.store');
|
||||
|
||||
const method = this.isEdit ? 'POST' : 'POST';
|
||||
|
||||
if (this.isEdit) {
|
||||
formData.append('_method', 'PUT');
|
||||
}
|
||||
|
||||
this.$inertia.post(url, formData, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
preserveScroll: false,
|
||||
onSuccess: (page) => {
|
||||
this.form.processing = false;
|
||||
},
|
||||
onError: (errors) => {
|
||||
|
||||
@@ -194,6 +194,13 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Link
|
||||
:href="route('dashboard.pages.edit', page.id)"
|
||||
class="p-2 text-muted-foreground-1 hover:text-primary hover:bg-primary/10 rounded-lg transition-all"
|
||||
title="Редактировать"
|
||||
>
|
||||
<DashboardIcon name="pencil-square" size="4" />
|
||||
</Link>
|
||||
<button
|
||||
@click.prevent="detachPage(page)"
|
||||
class="p-2 text-muted-foreground-1 hover:text-rose-600 hover:bg-rose-500/10 rounded-lg transition-all"
|
||||
@@ -224,6 +231,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import DashboardIcon from '../Components/DashboardIcon.vue';
|
||||
import FlashMessages from '../Components/shared/FlashMessages.vue';
|
||||
import EmptyState from '../Components/shared/EmptyState.vue';
|
||||
@@ -231,6 +239,7 @@ import EmptyState from '../Components/shared/EmptyState.vue';
|
||||
export default {
|
||||
name: 'SubSectionEdit',
|
||||
components: {
|
||||
Link,
|
||||
DashboardIcon,
|
||||
FlashMessages,
|
||||
EmptyState
|
||||
|
||||
Reference in New Issue
Block a user