ваше сообщение коммита

This commit is contained in:
2025-12-10 09:27:36 +03:00
parent 13ac707844
commit 863759e3e7
21 changed files with 1738 additions and 301 deletions

View File

@@ -26,6 +26,15 @@
<p>{{ isEditMode ? 'Редактируйте существующую страницу' : 'Создайте новую страницу для вашего DLE' }}</p>
</div>
<div class="header-actions">
<button
v-if="isEditMode && canManageLegalDocs && address"
class="btn btn-danger"
@click="deletePage"
type="button"
>
<i class="fas fa-trash"></i>
Удалить
</button>
<button class="close-btn" @click="goBack">×</button>
</div>
</div>
@@ -279,6 +288,24 @@ function goBack() {
router.push({ name: 'content-list' });
}
async function deletePage() {
if (!isEditMode.value || !editId.value) {
return;
}
if (!confirm('Вы уверены, что хотите удалить эту страницу? Это действие нельзя отменить. Все связанные файлы также будут удалены.')) {
return;
}
try {
await pagesService.deletePage(editId.value);
router.push({ name: 'content-list' });
} catch (error) {
console.error('Ошибка удаления страницы:', error);
alert('Ошибка при удалении страницы: ' + (error?.response?.data?.error || error?.message || 'Неизвестная ошибка'));
}
}
function onFileChange(e) {
const f = e.target.files && e.target.files[0];
if (f) {
@@ -432,8 +459,14 @@ async function handleSubmit() {
fd.append('status', form.value.status);
fd.append('settings', JSON.stringify(form.value.settings));
fd.append('visibility', form.value.visibility);
// Всегда отправляем required_permission:
// - Если visibility = public, отправляем пустую строку (будет установлен null на бэкенде)
// - Если visibility = internal, отправляем значение или пустую строку
if (form.value.visibility === 'internal' && form.value.requiredPermission) {
fd.append('required_permission', form.value.requiredPermission.trim());
} else {
// Явно устанавливаем пустое значение для public страниц
fd.append('required_permission', '');
}
fd.append('format', form.value.format);
if (fileBlob.value) {
@@ -470,8 +503,14 @@ async function handleSubmit() {
fd.append('status', form.value.status);
fd.append('settings', JSON.stringify(form.value.settings));
fd.append('visibility', form.value.visibility);
// Всегда отправляем required_permission:
// - Если visibility = public, отправляем пустую строку (будет установлен null на бэкенде)
// - Если visibility = internal, отправляем значение или пустую строку
if (form.value.visibility === 'internal' && form.value.requiredPermission) {
fd.append('required_permission', form.value.requiredPermission.trim());
} else {
// Явно устанавливаем пустое значение для public страниц
fd.append('required_permission', '');
}
fd.append('format', form.value.format);
fd.append('file', fileBlob.value);

View File

@@ -112,11 +112,12 @@ const filtered = computed(() => {
}
// Фильтр по правам доступа
// Если у документа нет required_permission, он доступен всем аутентифицированным пользователям
if (!p.required_permission) {
return false; // Документ без прав не показываем
return true; // Документ без прав доступен всем
}
// Проверяем права пользователя
// Проверяем права пользователя для документов с указанными правами
if (p.required_permission === PERMISSIONS.VIEW_BASIC_DOCS) {
return hasPermission(PERMISSIONS.VIEW_BASIC_DOCS);
}
@@ -129,7 +130,8 @@ const filtered = computed(() => {
return hasPermission(PERMISSIONS.MANAGE_LEGAL_DOCS);
}
return false;
// Если required_permission указан, но не распознан, показываем документ
return true;
});
});

View File

@@ -39,10 +39,14 @@
</div>
</div>
<div class="header-actions">
<button v-if="canEditData && address" class="btn btn-outline" @click="goToEdit">
<button v-if="canEditPage" class="btn btn-outline" @click="goToEdit">
<i class="fas fa-edit"></i>
Редактировать
</button>
<button v-if="canManageLegalDocs && address" class="btn btn-danger" @click="deletePage" type="button">
<i class="fas fa-trash"></i>
Удалить
</button>
<button class="close-btn" @click="goBack">×</button>
</div>
</div>
@@ -147,13 +151,14 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, watch, nextTick, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import pagesService from '../../services/pagesService';
import api from '../../api/axios';
import { useAuthContext } from '../../composables/useAuth';
import { usePermissions } from '../../composables/usePermissions';
import { PERMISSIONS } from '../../composables/permissions';
// Props
const props = defineProps({
@@ -184,9 +189,31 @@ const router = useRouter();
// Состояние
const page = ref(null);
const { address } = useAuthContext();
const { canEditData } = usePermissions();
const { canEditData, hasPermission } = usePermissions();
const canManageLegalDocs = computed(() => {
try {
return hasPermission(PERMISSIONS.MANAGE_LEGAL_DOCS);
} catch (e) {
console.error('[PageView] Ошибка проверки прав MANAGE_LEGAL_DOCS:', e);
return false;
}
});
const isLoading = ref(false);
// Удалять может ТОЛЬКО редактор (MANAGE_LEGAL_DOCS)
const canDeletePage = computed(() => {
const hasPermission = canManageLegalDocs.value;
const hasAddress = !!address.value;
console.log('[PageView] canDeletePage проверка:', { hasPermission, hasAddress, address: address.value });
return hasPermission && hasAddress;
});
// Редактировать может редактор или пользователь с правом редактирования
const canEditPage = computed(() => {
if (!address.value) return false;
return canManageLegalDocs.value || canEditData.value;
});
// Методы
function goToEdit() {
router.push({ name: 'content-create', query: { edit: route.params.id } });
@@ -202,14 +229,22 @@ async function reindex() {
}
async function deletePage() {
if (confirm('Вы уверены, что хотите удалить эту страницу? Это действие нельзя отменить.')) {
// Дополнительная проверка прав на стороне клиента
if (!canManageLegalDocs.value) {
alert('У вас нет прав для удаления страниц. Требуются права редактора.');
return;
}
if (!confirm('Вы уверены, что хотите удалить эту страницу? Это действие нельзя отменить. Все связанные файлы также будут удалены.')) {
return;
}
try {
await pagesService.deletePage(route.params.id);
router.push({ name: 'content-list' });
} catch (error) {
console.error('Ошибка удаления страницы:', error);
alert('Ошибка при удалении страницы');
}
alert('Ошибка при удалении страницы: ' + (error?.response?.data?.error || error?.message || 'Неизвестная ошибка'));
}
}
@@ -281,9 +316,57 @@ async function loadPage() {
}
}
// Обработка ошибок загрузки видео
function setupVideoErrorHandlers() {
nextTick(() => {
const videoElements = document.querySelectorAll('.page-content video');
videoElements.forEach((video) => {
video.addEventListener('error', (e) => {
console.error('Ошибка загрузки видео:', e);
const error = e.target.error;
let errorMessage = 'Неизвестная ошибка';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
errorMessage = 'Загрузка видео была прервана';
break;
case error.MEDIA_ERR_NETWORK:
errorMessage = 'Ошибка сети при загрузке видео';
break;
case error.MEDIA_ERR_DECODE:
errorMessage = 'Ошибка декодирования видео';
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMessage = 'Формат видео не поддерживается';
break;
default:
errorMessage = `Ошибка загрузки видео (код: ${error.code})`;
}
}
// Показываем сообщение об ошибке вместо видео
const errorDiv = document.createElement('div');
errorDiv.className = 'video-error';
errorDiv.style.cssText = 'padding: 20px; background: #fee; border: 1px solid #fcc; border-radius: 8px; margin: 1.5rem 0; color: #c33;';
errorDiv.textContent = `${errorMessage}`;
video.parentNode?.replaceChild(errorDiv, video);
});
});
});
}
// Отслеживание изменений контента для добавления обработчиков ошибок
watch(() => page.value?.content, () => {
if (page.value?.content) {
setupVideoErrorHandlers();
}
});
// Загрузка данных
onMounted(() => {
loadPage();
setupVideoErrorHandlers();
});
</script>

View File

@@ -106,7 +106,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, watch, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import pagesService from '../../services/pagesService';
@@ -199,9 +199,57 @@ async function loadPage() {
}
}
// Обработка ошибок загрузки видео
function setupVideoErrorHandlers() {
nextTick(() => {
const videoElements = document.querySelectorAll('.content-block video, .page-content video');
videoElements.forEach((video) => {
video.addEventListener('error', (e) => {
console.error('Ошибка загрузки видео:', e);
const error = e.target.error;
let errorMessage = 'Неизвестная ошибка';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
errorMessage = 'Загрузка видео была прервана';
break;
case error.MEDIA_ERR_NETWORK:
errorMessage = 'Ошибка сети при загрузке видео';
break;
case error.MEDIA_ERR_DECODE:
errorMessage = 'Ошибка декодирования видео';
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMessage = 'Формат видео не поддерживается';
break;
default:
errorMessage = `Ошибка загрузки видео (код: ${error.code})`;
}
}
// Показываем сообщение об ошибке вместо видео
const errorDiv = document.createElement('div');
errorDiv.className = 'video-error';
errorDiv.style.cssText = 'padding: 20px; background: #fee; border: 1px solid #fcc; border-radius: 8px; margin: 1.5rem 0; color: #c33;';
errorDiv.textContent = `${errorMessage}`;
video.parentNode?.replaceChild(errorDiv, video);
});
});
});
}
// Отслеживание изменений контента для добавления обработчиков ошибок
watch(() => page.value?.content, () => {
if (page.value?.content) {
setupVideoErrorHandlers();
}
});
// Загрузка данных
onMounted(() => {
loadPage();
setupVideoErrorHandlers();
});
</script>