changes
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Контактный виджет <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.contact"
|
||||
@change="update('contact', $event.target.value)"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите контакт...</option>
|
||||
<option v-for="contact in contacts" :key="contact.slug" :value="contact.slug">
|
||||
{{ contact.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ContactBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { contacts: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/contact-widgets/active');
|
||||
this.contacts = await response.json();
|
||||
} catch (e) { console.error('Failed to load contacts:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Форма <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.form"
|
||||
@change="update('form', $event.target.value)"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите форму...</option>
|
||||
<option v-for="form in forms" :key="form.form_id" :value="form.form_id">
|
||||
{{ form.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue.settings.in_modal"
|
||||
@change="updateSettings('in_modal', $event.target.checked)"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-layer-line rounded"
|
||||
/>
|
||||
<label class="text-sm text-foreground">
|
||||
Открывать в модальном окне
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomFormBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { forms: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/custom-forms/published');
|
||||
this.forms = await response.json();
|
||||
} catch (e) { console.error('Failed to load forms:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
updateSettings(field, value) {
|
||||
this.update('settings', { ...this.modelValue.settings, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(path, index) in modelValue.path"
|
||||
:key="index"
|
||||
class="flex items-center gap-3 p-3 border border-layer-line rounded-lg"
|
||||
>
|
||||
<div class="flex-1 text-sm text-foreground truncate">
|
||||
{{ path.split('/').pop() || path }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeFile(index)"
|
||||
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
@change="handleFilesUpload"
|
||||
accept=".pdf,.docx,.xlsx,.pptx,.zip"
|
||||
multiple
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground-1">
|
||||
PDF, DOCX, XLSX, PPTX, ZIP
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FastFilesBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
removeFile(index) {
|
||||
this.update('path', this.modelValue.path.filter((_, i) => i !== index));
|
||||
},
|
||||
handleFilesUpload(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
const newPaths = [...this.modelValue.path];
|
||||
files.forEach(file => {
|
||||
newPaths.push(URL.createObjectURL(file));
|
||||
});
|
||||
this.update('path', newPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(file, index) in modelValue.file"
|
||||
:key="index"
|
||||
class="flex items-center gap-3 p-3 border border-layer-line rounded-lg"
|
||||
>
|
||||
<div class="flex-1 grid grid-cols-2 gap-2">
|
||||
<input
|
||||
:value="file.title"
|
||||
@input="updateFile(index, 'title', $event.target.value)"
|
||||
type="text"
|
||||
placeholder="Название файла"
|
||||
class="px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground text-sm"
|
||||
/>
|
||||
<div class="text-xs text-muted-foreground-1 flex items-center">
|
||||
<span>{{ file.expansion?.toUpperCase() || '—' }}</span>
|
||||
<span class="mx-1">•</span>
|
||||
<span>{{ file.size || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeFile(index)"
|
||||
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
@change="handleFilesUpload"
|
||||
accept=".pdf,.docx,.xlsx,.pptx,.zip"
|
||||
multiple
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground-1">
|
||||
PDF, DOCX, XLSX, PPTX, ZIP. Макс. 512MB
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addFile"
|
||||
class="text-sm text-primary hover:text-primary-hover transition-colors"
|
||||
>
|
||||
+ Добавить файл
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FilesBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
updateFile(index, field, value) {
|
||||
const files = [...this.modelValue.file];
|
||||
files[index] = { ...files[index], [field]: value };
|
||||
this.update('file', files);
|
||||
},
|
||||
addFile() {
|
||||
this.update('file', [...this.modelValue.file, { title: '', path: '', expansion: '', size: '', time_added: Date.now() }]);
|
||||
},
|
||||
removeFile(index) {
|
||||
this.update('file', this.modelValue.file.filter((_, i) => i !== index));
|
||||
},
|
||||
handleFilesUpload(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
files.forEach(file => {
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
const size = (file.size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
this.update('file', [...this.modelValue.file, {
|
||||
title: file.name,
|
||||
path: URL.createObjectURL(file),
|
||||
expansion: extension,
|
||||
size: size,
|
||||
time_added: Date.now()
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Заголовок <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.content"
|
||||
@input="update('content', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Введите заголовок раздела"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Якорь (ID)
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.id"
|
||||
@input="update('id', $event.target.value)"
|
||||
type="text"
|
||||
placeholder="anchor-link-..."
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-muted text-muted-foreground-1 text-sm"
|
||||
readonly
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground-1">Генерируется автоматически</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HeadingBlock',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
[field]: value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Изображение <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-if="modelValue.url" class="relative">
|
||||
<img :src="modelValue.url" alt="Preview" class="w-32 h-32 object-cover rounded-lg border border-layer-line" />
|
||||
<button
|
||||
type="button"
|
||||
@click="update('url', '')"
|
||||
class="absolute -top-2 -right-2 p-1 bg-danger text-white rounded-full hover:bg-danger-hover transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
accept="image/*"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground-1">
|
||||
Загрузите изображение (JPG, PNG, WebP)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Alt текст
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.alt"
|
||||
@input="update('alt', $event.target.value)"
|
||||
type="text"
|
||||
placeholder="Описание изображения"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ImageBlock',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
[field]: value
|
||||
});
|
||||
},
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.update('url', e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Изображения <span class="text-danger">*</span> (макс. 5)
|
||||
</label>
|
||||
<div class="grid grid-cols-3 gap-3 mb-3">
|
||||
<div
|
||||
v-for="(url, index) in modelValue.url"
|
||||
:key="index"
|
||||
class="relative group"
|
||||
>
|
||||
<img :src="url" alt="Preview" class="w-full h-24 object-cover rounded-lg border border-layer-line" />
|
||||
<button
|
||||
type="button"
|
||||
@click="removeImage(index)"
|
||||
class="absolute -top-2 -right-2 p-1 bg-danger text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handleFilesUpload"
|
||||
accept="image/*"
|
||||
multiple
|
||||
:disabled="modelValue.url.length >= 5"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground-1">
|
||||
Можно загрузить до 5 изображений (JPG, PNG, WebP)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Alt текст
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.alt"
|
||||
@input="update('alt', $event.target.value)"
|
||||
type="text"
|
||||
placeholder="Описание изображений"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ImagesBlock',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
[field]: value
|
||||
});
|
||||
},
|
||||
handleFilesUpload(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
const remaining = 5 - this.modelValue.url.length;
|
||||
const toProcess = files.slice(0, remaining);
|
||||
|
||||
let processed = 0;
|
||||
toProcess.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const newUrls = [...this.modelValue.url, e.target.result];
|
||||
this.update('url', newUrls);
|
||||
processed++;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
},
|
||||
removeImage(index) {
|
||||
const newUrls = this.modelValue.url.filter((_, i) => i !== index);
|
||||
this.update('url', newUrls);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Страница <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.page"
|
||||
@change="update('page', parseInt($event.target.value))"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option :value="null">Выберите страницу...</option>
|
||||
<option v-for="page in pages" :key="page.id" :value="page.id">
|
||||
{{ page.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageItemBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { pages: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/pages/visible');
|
||||
this.pages = await response.json();
|
||||
} catch (e) { console.error('Failed to load pages:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Список ресурсов <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.resource"
|
||||
@change="update('resource', $event.target.value)"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите ресурс...</option>
|
||||
<option v-for="resource in resources" :key="resource.slug" :value="resource.slug">
|
||||
{{ resource.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PageResourceListBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { resources: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/page-reference-lists/active');
|
||||
this.resources = await response.json();
|
||||
} catch (e) { console.error('Failed to load resources:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<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>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Текст <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
:id="editorId"
|
||||
:value="modelValue.content"
|
||||
rows="8"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-y"
|
||||
></textarea>
|
||||
<p v-if="loading" class="mt-1 text-xs text-primary">
|
||||
Загрузка редактора...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let editorCounter = 0;
|
||||
|
||||
export default {
|
||||
name: 'ParagraphBlock',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
editorId: `paragraph-editor-${++editorCounter}`,
|
||||
editor: null,
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.waitForTinyMCE();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroyEditor();
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
[field]: value
|
||||
});
|
||||
},
|
||||
waitForTinyMCE() {
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
this.initEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ждем загрузки TinyMCE с таймаутом
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50; // 5 секунд
|
||||
const interval = setInterval(() => {
|
||||
attempts++;
|
||||
if (typeof tinymce !== 'undefined') {
|
||||
clearInterval(interval);
|
||||
this.initEditor();
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(interval);
|
||||
this.loading = false;
|
||||
console.error('TinyMCE не загрузился в течение 5 секунд');
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
initEditor() {
|
||||
this.loading = true;
|
||||
|
||||
tinymce.init({
|
||||
selector: `#${this.editorId}`,
|
||||
license_key: 'gpl',
|
||||
autoresize_bottom_margin: 20,
|
||||
autoresize_overflow_padding: 20,
|
||||
max_height: 600,
|
||||
menubar: false,
|
||||
statusbar: true,
|
||||
branding: false,
|
||||
plugins: [
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
|
||||
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
|
||||
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount',
|
||||
'autoresize'
|
||||
],
|
||||
toolbar: 'undo redo | blocks | ' +
|
||||
'bold italic forecolor | alignleft aligncenter ' +
|
||||
'alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'removeformat | help',
|
||||
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;
|
||||
});
|
||||
editor.on('change', () => {
|
||||
this.update('content', editor.getContent());
|
||||
});
|
||||
editor.on('Remove', () => {
|
||||
this.editor = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
destroyEditor() {
|
||||
if (this.editor && typeof tinymce !== 'undefined') {
|
||||
tinymce.remove(this.editor);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
ФИО <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.name"
|
||||
@input="update('name', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Иванов Иван Иванович"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Фото
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
@change="handlePhotoUpload"
|
||||
accept="image/*"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="block text-sm font-medium text-foreground">
|
||||
Информация <span class="text-danger">*</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="addInfoRow"
|
||||
class="text-sm text-primary hover:text-primary-hover transition-colors"
|
||||
>
|
||||
+ Добавить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(info, index) in modelValue.info"
|
||||
:key="index"
|
||||
class="grid grid-cols-2 gap-2 p-3 border border-layer-line rounded-lg"
|
||||
>
|
||||
<input
|
||||
:value="info.column"
|
||||
@input="updateInfo(index, 'column', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Должность"
|
||||
class="px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground text-sm"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<textarea
|
||||
:value="info.content"
|
||||
@input="updateInfo(index, 'content', $event.target.value)"
|
||||
required
|
||||
maxlength="1000"
|
||||
placeholder="Описание"
|
||||
rows="2"
|
||||
class="flex-1 px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground text-sm resize-none"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeInfo(index)"
|
||||
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all self-center"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PersonBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
updateInfo(index, field, value) {
|
||||
const info = [...this.modelValue.info];
|
||||
info[index] = { ...info[index], [field]: value };
|
||||
this.update('info', info);
|
||||
},
|
||||
addInfoRow() {
|
||||
this.update('info', [...this.modelValue.info, { column: '', content: '' }]);
|
||||
},
|
||||
removeInfo(index) {
|
||||
if (this.modelValue.info.length > 1) {
|
||||
this.update('info', this.modelValue.info.filter((_, i) => i !== index));
|
||||
}
|
||||
},
|
||||
handlePhotoUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => this.update('photo', e.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Новость <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.post"
|
||||
@change="update('post', parseInt($event.target.value))"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option :value="null">Выберите новость...</option>
|
||||
<option v-for="post in posts" :key="post.id" :value="post.id">
|
||||
{{ post.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PostItemBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { posts: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/posts/published?limit=100');
|
||||
this.posts = await response.json();
|
||||
} catch (e) { console.error('Failed to load posts:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Количество
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.count"
|
||||
@input="update('count', parseInt($event.target.value) || 5)"
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Категория
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.category"
|
||||
@change="update('category', $event.target.value ? parseInt($event.target.value) : null)"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option :value="null">Все категории</option>
|
||||
<option v-for="cat in categories" :key="cat.id" :value="cat.id">
|
||||
{{ cat.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PostListBlock',
|
||||
props: { modelValue: { type: Object, required: true } },
|
||||
emits: ['update:modelValue'],
|
||||
data() { return { categories: [] }; },
|
||||
async mounted() {
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/categories');
|
||||
this.categories = await response.json();
|
||||
} catch (e) { console.error('Failed to load categories:', e); }
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Слайдер <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue.slider"
|
||||
@change="update('slider', $event.target.value)"
|
||||
required
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите слайдер...</option>
|
||||
<option v-for="slider in sliders" :key="slider.slug" :value="slider.slug">
|
||||
{{ slider.title }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-muted-foreground-1">
|
||||
Только активные слайдеры с изображениями
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SliderBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return { sliders: [] };
|
||||
},
|
||||
async mounted() {
|
||||
// Загрузка слайдеров с бэкенда
|
||||
try {
|
||||
const response = await fetch('/api/dashboard/sliders/active');
|
||||
this.sliders = await response.json();
|
||||
} catch (e) {
|
||||
console.error('Failed to load sliders:', e);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Название процесса <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.step_name"
|
||||
@input="update('step_name', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Процесс оформления"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="block text-sm font-medium text-foreground">
|
||||
Шаги <span class="text-danger">*</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="addStep"
|
||||
class="text-sm text-primary hover:text-primary-hover transition-colors"
|
||||
>
|
||||
+ Добавить шаг
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(step, index) in modelValue.steps"
|
||||
:key="index"
|
||||
class="p-4 border border-layer-line rounded-lg space-y-2"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-medium">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<input
|
||||
:value="step.title"
|
||||
@input="updateStep(index, 'title', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Название шага"
|
||||
class="flex-1 px-3 py-1.5 border border-layer-line rounded-lg bg-white text-foreground text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeStep(index)"
|
||||
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all"
|
||||
:disabled="modelValue.steps.length <= 1"
|
||||
title="Удалить"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
:id="`step-content-${index}-${_uid}`"
|
||||
:value="step.content"
|
||||
@input="updateStep(index, 'content', $event.target.value)"
|
||||
required
|
||||
rows="3"
|
||||
placeholder="Описание шага"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground text-sm resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StepperBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
updateStep(index, field, value) {
|
||||
const steps = [...this.modelValue.steps];
|
||||
steps[index] = { ...steps[index], [field]: value };
|
||||
this.update('steps', steps);
|
||||
},
|
||||
addStep() {
|
||||
this.update('steps', [...this.modelValue.steps, { title: '', content: '' }]);
|
||||
},
|
||||
removeStep(index) {
|
||||
if (this.modelValue.steps.length > 1) {
|
||||
this.update('steps', this.modelValue.steps.filter((_, i) => i !== index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Settings -->
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="modelValue.settings.is_accordion"
|
||||
@change="updateSettings('is_accordion', $event.target.checked)"
|
||||
class="h-4 w-4 text-primary focus:ring-primary border-layer-line rounded"
|
||||
/>
|
||||
<label class="text-sm text-foreground">
|
||||
Режим аккордеона (только одна вкладка открыта)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="block text-sm font-medium text-foreground">
|
||||
Вкладки <span class="text-danger">*</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
@click="addTab"
|
||||
class="text-sm text-primary hover:text-primary-hover transition-colors"
|
||||
>
|
||||
+ Добавить вкладку
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(tab, tabIndex) in tabs"
|
||||
:key="tab._uid"
|
||||
class="border border-layer-line rounded-lg overflow-hidden"
|
||||
>
|
||||
<!-- Tab Header -->
|
||||
<div class="flex items-center gap-2 px-4 py-3 bg-muted/30 border-b border-layer-line">
|
||||
<input
|
||||
:value="tab.title"
|
||||
@input="updateTab(tabIndex, 'title', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Название вкладки"
|
||||
class="flex-1 px-3 py-1.5 border border-layer-line rounded-lg bg-white text-foreground text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeTab(tabIndex)"
|
||||
class="p-1.5 text-muted-foreground-1 hover:text-danger hover:bg-danger/10 rounded transition-all"
|
||||
:disabled="tabs.length <= 1"
|
||||
title="Удалить вкладку"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content (Nested TabContentBuilder) -->
|
||||
<div class="p-4">
|
||||
<TabContentBuilder
|
||||
:model-value="tab.content"
|
||||
@update:model-value="updateTab(tabIndex, 'content', $event)"
|
||||
label="Содержимое вкладки"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TabContentBuilder from '../TabContentBuilder.vue';
|
||||
|
||||
export default {
|
||||
name: 'TabBlock',
|
||||
components: {
|
||||
TabContentBuilder
|
||||
},
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
tabs() {
|
||||
const rawTabs = this.modelValue?.tab || [];
|
||||
// Нормализуем _uid для всех вкладок (если пришли с сервера без UID)
|
||||
return rawTabs.map(tab => ({
|
||||
...tab,
|
||||
_uid: tab._uid || `tab-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
updateSettings(field, value) {
|
||||
this.update('settings', { ...this.modelValue.settings, [field]: value });
|
||||
},
|
||||
updateTab(index, field, value) {
|
||||
const tabs = [...this.tabs];
|
||||
tabs[index] = { ...tabs[index], [field]: value };
|
||||
this.update('tab', tabs);
|
||||
},
|
||||
addTab() {
|
||||
const newTab = {
|
||||
_uid: `tab-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||
title: '',
|
||||
content: []
|
||||
};
|
||||
this.update('tab', [...this.tabs, newTab]);
|
||||
},
|
||||
removeTab(index) {
|
||||
if (this.tabs.length > 1) {
|
||||
this.update('tab', this.tabs.filter((_, i) => i !== index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Видеофайл <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div v-if="modelValue.path" class="mb-3">
|
||||
<video :src="modelValue.path" controls class="w-full max-w-md rounded-lg border border-layer-line"></video>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handleFileUpload"
|
||||
accept="video/*"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground-1">
|
||||
mp4, mov, avi, webm, ogg
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-1">
|
||||
Название видео <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.title"
|
||||
@input="update('title', $event.target.value)"
|
||||
type="text"
|
||||
required
|
||||
maxlength="255"
|
||||
placeholder="Введите название видео"
|
||||
class="w-full px-3 py-2 border border-layer-line rounded-lg bg-white text-foreground placeholder-muted-foreground-1 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VideoBlock',
|
||||
props: {
|
||||
modelValue: { type: Object, required: true }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
update(field, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [field]: value });
|
||||
},
|
||||
handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.update('mime', file.type);
|
||||
this.update('path', URL.createObjectURL(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user