changes
This commit is contained in:
@@ -291,7 +291,7 @@ export default {
|
||||
|
||||
const blockDefaults = {
|
||||
heading: () => ({ id: `anchor-${Date.now()}`, content: '' }),
|
||||
paragraph: () => ({ seo_active: true, content: '' }),
|
||||
paragraph: () => ({ content: '' }),
|
||||
image: () => ({ url: '', alt: '' }),
|
||||
images: () => ({ url: [], alt: '' }),
|
||||
files: () => ({ file: [] }),
|
||||
|
||||
@@ -288,7 +288,7 @@ export default {
|
||||
|
||||
const blockDefaults = {
|
||||
heading: () => ({ id: `anchor-${Date.now()}`, content: '' }),
|
||||
paragraph: () => ({ seo_active: true, content: '' }),
|
||||
paragraph: () => ({ content: '' }),
|
||||
image: () => ({ url: '', alt: '' }),
|
||||
images: () => ({ url: [], alt: '' }),
|
||||
files: () => ({ file: [] }),
|
||||
@@ -402,24 +402,26 @@ export default {
|
||||
onMounted(() => {
|
||||
if (props.modelValue && props.modelValue.length > 0) {
|
||||
blocks.value = props.modelValue.map(block => ({
|
||||
_uid: generateUid(),
|
||||
_uid: block._uid || generateUid(),
|
||||
...block
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for modelValue changes (e.g., when loading existing content)
|
||||
// Watch for modelValue changes — only for initial load or when parent reloads
|
||||
// DO NOT sync on every change to avoid infinite loops with emitChange
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
// Only update if blocks are empty (page reload or tab switch)
|
||||
if (newValue && newValue.length > 0 && blocks.value.length === 0) {
|
||||
// Only initialize if blocks are empty (initial load from server)
|
||||
blocks.value = newValue.map(block => ({
|
||||
_uid: generateUid(),
|
||||
_uid: block._uid || generateUid(),
|
||||
...block
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<div class="space-y-3" style="overflow: visible;">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue.seo_active"
|
||||
@change="update('seo_active', $event.target.checked)"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-layer-line rounded"
|
||||
/>
|
||||
<label class="text-sm text-foreground">
|
||||
Активировать SEO для этого блока
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="overflow: visible;">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Текст <span class="text-danger">*</span>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
emits: ['update:modelValue', 'update'],
|
||||
computed: {
|
||||
tabs() {
|
||||
const rawTabs = this.modelValue?.tab || [];
|
||||
@@ -95,6 +95,7 @@ export default {
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
this.$emit('update');
|
||||
},
|
||||
updateSettings(field, value) {
|
||||
this.update('settings', { ...(this.modelValue.settings || {}), [field]: value });
|
||||
|
||||
@@ -333,7 +333,6 @@ export default {
|
||||
sub_section_id: '',
|
||||
code: '200',
|
||||
searchable: true,
|
||||
icon: 'heroicon-o-academic-cap',
|
||||
content: [],
|
||||
settings: {
|
||||
hide_page_sub_section_links: false,
|
||||
|
||||
@@ -350,7 +350,6 @@ export default {
|
||||
sub_section_id: this.page.sub_section_id || '',
|
||||
code: this.page.code || '200',
|
||||
searchable: this.page.searchable === 1 || this.page.searchable === true || this.page.searchable === '1',
|
||||
icon: this.page.icon || 'heroicon-o-academic-cap',
|
||||
content: this.page.content || [],
|
||||
settings: {
|
||||
hide_page_sub_section_links: this.page.settings?.hide_page_sub_section_links || false,
|
||||
|
||||
@@ -161,10 +161,13 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto" :class="{ 'select-none': dragIndex !== null }">
|
||||
<table class="min-w-full divide-y divide-line-2">
|
||||
<thead class="bg-surface/50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider w-10">
|
||||
<!-- Drag handle column -->
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-muted-foreground-1 uppercase tracking-wider">
|
||||
Заголовок страницы
|
||||
</th>
|
||||
@@ -178,10 +181,26 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-line-2">
|
||||
<tr
|
||||
v-for="page in pages"
|
||||
v-for="(page, index) in pages"
|
||||
:key="page.id"
|
||||
class="group hover:bg-muted-hover/50 transition-all duration-200"
|
||||
:data-index="index"
|
||||
:draggable="true"
|
||||
@dragstart="onDragStart($event, index)"
|
||||
@dragover.prevent="onDragOver($event, index)"
|
||||
@dragenter.prevent="onDragEnter($event, index)"
|
||||
@drop.prevent="onDrop($event, index)"
|
||||
@dragend="onDragEnd"
|
||||
:class="[
|
||||
'group hover:bg-muted-hover/50 transition-all duration-200',
|
||||
dragIndex === index ? 'opacity-40 bg-primary/10 border-y-2 border-primary/30' : '',
|
||||
dropIndex === index && dragIndex !== index ? 'border-t-2 border-primary bg-primary/5' : ''
|
||||
]"
|
||||
>
|
||||
<td class="px-6 py-4">
|
||||
<div class="cursor-move text-muted-foreground-1 hover:text-foreground transition-colors">
|
||||
<DashboardIcon name="bars-3" size="4" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-foreground">
|
||||
{{ page.title }}
|
||||
@@ -215,7 +234,7 @@
|
||||
<!-- Empty State -->
|
||||
<EmptyState
|
||||
v-if="pages.length === 0"
|
||||
:columns="3"
|
||||
:columns="4"
|
||||
title="Страницы не найдены"
|
||||
description="Прикрепите страницы к этому подразделу"
|
||||
icon-path="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
@@ -223,6 +242,28 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Save Order Button (shown when order changed) -->
|
||||
<div v-if="orderChanged" class="px-6 py-4 border-t border-line-2 bg-surface/30">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-muted-foreground-1">
|
||||
<DashboardIcon name="information-circle" size="4" class="inline mr-1" />
|
||||
Порядок страниц изменен. Сохраните изменения.
|
||||
</p>
|
||||
<button
|
||||
@click="saveOrder"
|
||||
:disabled="savingOrder"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 bg-primary text-white text-sm font-medium rounded-lg hover:bg-primary-hover transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg v-if="savingOrder" class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<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" />
|
||||
</svg>
|
||||
<DashboardIcon v-else name="check" size="4" />
|
||||
{{ savingOrder ? 'Сохранение...' : 'Сохранить порядок' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,13 +312,19 @@ export default {
|
||||
selectedPageId: '',
|
||||
errors: {},
|
||||
processing: false,
|
||||
attachingPage: false
|
||||
attachingPage: false,
|
||||
// Drag and drop state
|
||||
dragIndex: null,
|
||||
dropIndex: null,
|
||||
localPages: [],
|
||||
orderChanged: false,
|
||||
savingOrder: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pages() {
|
||||
return this.subSection.pages || [];
|
||||
return this.orderChanged ? this.localPages : (this.subSection.pages || []);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -316,9 +363,17 @@ export default {
|
||||
{ page_id: this.selectedPageId },
|
||||
{
|
||||
preserveScroll: true,
|
||||
onFinish: () => {
|
||||
onSuccess: () => {
|
||||
this.attachingPage = false;
|
||||
this.selectedPageId = '';
|
||||
// Reset local changes if page is attached
|
||||
if (this.orderChanged) {
|
||||
this.orderChanged = false;
|
||||
this.localPages = [];
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
this.attachingPage = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -326,13 +381,94 @@ export default {
|
||||
|
||||
detachPage(page) {
|
||||
if (confirm(`Открепить страницу "${page.title}"?`)) {
|
||||
// If we have local changes, remove from local state first
|
||||
if (this.orderChanged) {
|
||||
const index = this.localPages.findIndex(p => p.id === page.id);
|
||||
if (index !== -1) {
|
||||
this.localPages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.$inertia.delete(route('dashboard.sub-sections.pages.detach', {
|
||||
subSection: this.subSection.id,
|
||||
page: page.id
|
||||
}), {
|
||||
preserveScroll: true
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// Reset local changes if page is detached
|
||||
if (this.orderChanged && this.localPages.length === 0) {
|
||||
this.orderChanged = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Drag and Drop methods
|
||||
onDragStart(event, index) {
|
||||
this.dragIndex = index;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', index.toString());
|
||||
},
|
||||
|
||||
onDragOver(event, index) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
},
|
||||
|
||||
onDragEnter(event, index) {
|
||||
this.dropIndex = index;
|
||||
},
|
||||
|
||||
onDrop(event, targetIndex) {
|
||||
const sourceIndex = this.dragIndex;
|
||||
|
||||
if (sourceIndex === null || sourceIndex === targetIndex) {
|
||||
this.dragIndex = null;
|
||||
this.dropIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize localPages if not already done
|
||||
if (!this.orderChanged) {
|
||||
this.localPages = [...this.pages];
|
||||
}
|
||||
|
||||
// Move the item in the array
|
||||
const item = this.localPages.splice(sourceIndex, 1)[0];
|
||||
this.localPages.splice(targetIndex, 0, item);
|
||||
|
||||
this.orderChanged = true;
|
||||
this.dragIndex = null;
|
||||
this.dropIndex = null;
|
||||
},
|
||||
|
||||
onDragEnd() {
|
||||
this.dragIndex = null;
|
||||
this.dropIndex = null;
|
||||
},
|
||||
|
||||
saveOrder() {
|
||||
if (!this.orderChanged) return;
|
||||
|
||||
this.savingOrder = true;
|
||||
|
||||
const pageIds = this.localPages.map(page => page.id);
|
||||
|
||||
this.$inertia.post(
|
||||
route('dashboard.sub-sections.pages.reorder', this.subSection.id),
|
||||
{ page_ids: pageIds },
|
||||
{
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
this.savingOrder = false;
|
||||
this.orderChanged = false;
|
||||
this.localPages = [];
|
||||
},
|
||||
onError: () => {
|
||||
this.savingOrder = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user