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

This commit is contained in:
2025-12-09 15:00:57 +03:00
parent d716e3f077
commit 13ac707844
13 changed files with 1458 additions and 62 deletions

View File

@@ -452,6 +452,7 @@ onMounted(() => {
margin: 0 auto;
padding: 40px;
min-height: 100%;
width: 100%;
}
.back-btn {
@@ -713,7 +714,7 @@ onMounted(() => {
max-width: 100%;
width: 100%;
height: auto;
min-height: 300px;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
@@ -721,6 +722,12 @@ onMounted(() => {
background: #000;
}
.content-text :deep(video.ql-video) {
width: 100%;
max-width: 100%;
min-height: 400px;
}
.content-text :deep(video:focus) {
outline: 2px solid var(--color-primary);
outline-offset: 2px;

View File

@@ -22,13 +22,23 @@ import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import api from '../../api/axios';
// Импортируем и регистрируем модуль изменения размера изображений
let ImageResize;
try {
ImageResize = require('quill-image-resize-module').default || require('quill-image-resize-module');
Quill.register('modules/imageResize', ImageResize);
} catch (error) {
console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error);
// Функция для загрузки и регистрации модуля изменения размера изображений
async function loadImageResizeModule() {
try {
// Используем динамический импорт для совместимости с Vite
const module = await import('quill-image-resize-module');
const ImageResize = module.default || module.ImageResize || module;
if (ImageResize && typeof ImageResize === 'function') {
Quill.register('modules/imageResize', ImageResize);
return true;
} else if (ImageResize && ImageResize.default && typeof ImageResize.default === 'function') {
Quill.register('modules/imageResize', ImageResize.default);
return true;
}
} catch (error) {
console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error);
}
return false;
}
const props = defineProps({
@@ -63,26 +73,36 @@ const toolbarOptions = [
['clean']
];
onMounted(() => {
onMounted(async () => {
if (!editorContainer.value) return;
// Загружаем модуль изменения размера изображений перед инициализацией
const imageResizeLoaded = await loadImageResizeModule();
// Конфигурация модулей
const modulesConfig = {
toolbar: {
container: toolbarOptions,
handlers: {
'image': handleImageClick,
'video': handleVideoClick
}
}
};
// Добавляем imageResize только если модуль загружен
if (imageResizeLoaded) {
modulesConfig.imageResize = {
parchment: Quill.import('parchment'),
modules: ['Resize', 'DisplaySize', 'Toolbar']
};
}
// Инициализация Quill
quill = new Quill(editorContainer.value, {
theme: 'snow',
placeholder: props.placeholder,
modules: {
toolbar: {
container: toolbarOptions,
handlers: {
'image': handleImageClick,
'video': handleVideoClick
}
},
imageResize: {
parchment: Quill.import('parchment'),
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
}
modules: modulesConfig
});
// Устанавливаем начальное значение
@@ -368,14 +388,55 @@ defineExpose({
padding: 4px 8px;
}
:deep(.ql-snow img),
:deep(.ql-snow video) {
:deep(.ql-snow img) {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 10px 0;
}
/* Стили для видео в редакторе - на всю ширину */
:deep(.ql-snow video),
:deep(.ql-editor video) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
}
:deep(.ql-snow video.ql-video),
:deep(.ql-editor video.ql-video) {
width: 100%;
max-width: 100%;
min-height: 400px;
}
/* Стили для iframe в редакторе */
:deep(.ql-snow iframe),
:deep(.ql-editor iframe) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
border: none;
}
:deep(.ql-snow iframe.ql-video),
:deep(.ql-editor iframe.ql-video) {
min-height: 400px;
aspect-ratio: 16 / 9;
}
/* Стили для изменения размера изображений */
:deep(.ql-image-resize) {
display: inline-block;
@@ -394,13 +455,5 @@ defineExpose({
cursor: nwse-resize;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
:deep(.ql-snow img),
:deep(.ql-snow video) {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 10px 0;
}
</style>

View File

@@ -569,6 +569,7 @@ onMounted(async () => {
border: 1px solid #e9ecef;
max-width: 1000px;
margin: 0 auto;
width: 100%;
}
.form-section {
@@ -635,6 +636,64 @@ onMounted(async () => {
color: var(--color-grey-dark);
}
/* Стили для видео в редакторе */
.content-form :deep(video) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
}
.content-form :deep(video.ql-video) {
width: 100%;
max-width: 100%;
min-height: 400px;
}
.content-form :deep(video:focus) {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Стили для iframe в редакторе (для внешних видео) */
.content-form :deep(iframe) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
border: none;
}
.content-form :deep(iframe.ql-video) {
min-height: 400px;
aspect-ratio: 16 / 9;
}
.content-form :deep(iframe:focus) {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Стили для изображений в редакторе */
.content-form :deep(img) {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.category-select-wrapper {
display: flex;
gap: 10px;

View File

@@ -396,10 +396,16 @@
<div class="ssl-section">
<div class="section-header">
<h2>SSL сертификат</h2>
<div v-if="isDevelopment" style="font-size: 12px; color: #666; margin-top: 5px;">
Debug: isEditor={{ isEditor }}, currentRole={{ currentRole }}, isLoadingSsl={{ isLoadingSsl }}
</div>
</div>
<div v-if="!isEditor" class="access-denied-message">
<p> Управление SSL доступно только пользователям с ролью "Редактор"</p>
<p v-if="isDevelopment" style="font-size: 12px; color: #666;">
Текущая роль: {{ currentRole }}
</p>
</div>
<div v-else>
@@ -407,7 +413,7 @@
<div v-if="isLoadingSsl">
Загрузка статуса SSL...
</div>
<div v-else>
<div v-else>
<div v-if="sslStatus && sslStatus.success && sslStatus.allCertificates && sslStatus.allCertificates.length">
<div class="ssl-info">
<div
@@ -415,15 +421,24 @@
:key="cert.name"
class="ssl-info-item"
>
<label>{{ cert.name }}</label>
<label>{{ cert.name || 'Без имени' }}</label>
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
{{ cert.expiryDate || 'Без данных' }}
{{ cert.expiryDate ? formatDate(cert.expiryDate) : 'Без данных' }}
</span>
<div v-if="cert.domains && cert.domains.length" class="ssl-domains">
Домены: {{ cert.domains.join(', ') }}
</div>
</div>
</div>
</div>
<div v-else class="ssl-no-cert">
SSL сертификат не найден для текущего домена.
<p>SSL сертификат не найден для текущего домена.</p>
<p v-if="sslStatus && sslStatus.domain" class="ssl-domain-info">
Домен: {{ sslStatus.domain }}
</p>
<p v-if="sslStatus && !sslStatus.success" class="ssl-error-info">
Ошибка: {{ sslStatus.error || 'Неизвестная ошибка' }}
</p>
</div>
</div>
</div>
@@ -441,9 +456,13 @@
class="action-btn ssl-btn renew"
:disabled="isLoading"
@click="renewSslCertificate"
:title="isLoading ? 'Выполняется...' : 'Получить или обновить SSL сертификат'"
>
🔐 Получить / обновить SSL
</button>
<div v-if="!isEditor && isDevelopment" style="font-size: 12px; color: #f00; margin-top: 5px;">
Кнопка скрыта: isEditor=false, currentRole={{ currentRole }}
</div>
</div>
</div>
</div>
@@ -550,6 +569,9 @@ const router = useRouter();
const { currentRole, canManageSettings } = usePermissions();
const isEditor = computed(() => currentRole.value === ROLES.EDITOR);
// Отладочная информация (только для разработки)
const isDevelopment = computed(() => import.meta.env.DEV || import.meta.env.MODE === 'development');
// Состояние
const domain = ref(null);
const isOnline = ref(false);
@@ -1223,13 +1245,21 @@ const sendBackup = async () => {
const loadSslStatus = async () => {
if (!isEditor.value) {
// Не показываем ошибку, если пользователь не редактор - просто не загружаем статус
console.log('[VDS] Пользователь не является редактором, пропускаем загрузку SSL статуса');
return;
}
console.log('[VDS] Загрузка SSL статуса...');
isLoadingSsl.value = true;
try {
const response = await axios.get('/vds/ssl/status');
console.log('[VDS] Ответ от /vds/ssl/status:', response.data);
if (response.data.success) {
sslStatus.value = response.data;
console.log('[VDS] SSL статус загружен:', {
hasCertificates: response.data.allCertificates?.length > 0,
certificatesCount: response.data.allCertificates?.length || 0,
domain: response.data.domain
});
} else {
console.warn('[VDS] Получение статуса SSL не успешно:', response.data);
sslStatus.value = null;
@@ -1238,16 +1268,24 @@ const loadSslStatus = async () => {
}
} catch (error) {
console.error('Ошибка получения статуса SSL:', error);
console.error('Детали ошибки:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
message: error.message
});
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
// Если VDS не настроена, это нормальная ситуация - не показываем ошибку
if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) {
console.log('[VDS] VDS не настроена, это нормально');
sslStatus.value = null;
return;
}
// Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
console.log('[VDS] Ошибка аутентификации, это нормально');
sslStatus.value = null;
return;
}
@@ -1293,26 +1331,38 @@ const checkSslStatus = async () => {
};
const renewSslCertificate = async () => {
console.log('[VDS] renewSslCertificate вызвана, isEditor:', isEditor.value);
if (!isEditor.value) {
console.warn('[VDS] Пользователь не является редактором, доступ запрещен');
alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты');
return;
}
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) {
console.log('[VDS] Пользователь отменил получение SSL сертификата');
return;
}
console.log('[VDS] Начинаем получение SSL сертификата...');
isLoading.value = true;
try {
const response = await axios.post('/vds/ssl/renew', {
sslProvider: 'letsencrypt'
});
console.log('[VDS] Ответ от /vds/ssl/renew:', response.data);
if (response.data.success) {
alert('SSL сертификат успешно получен/обновлен');
await loadSslStatus();
} else {
console.error('[VDS] Ошибка получения SSL сертификата:', response.data);
alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка получения SSL сертификата:', error);
console.error('Детали ошибки:', {
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
message: error.message
});
const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
const errorDetails = error.response?.data?.details || '';
@@ -1519,6 +1569,7 @@ const updateCharts = () => {
// Жизненный цикл
onMounted(async () => {
console.log('[VDS] Компонент монтирован, isEditor:', isEditor.value, 'currentRole:', currentRole.value);
await loadSettings();
await loadContainers();
await initCharts();
@@ -1526,8 +1577,11 @@ onMounted(async () => {
// Загружаем пользователей только для редакторов
if (isEditor.value) {
console.log('[VDS] Пользователь является редактором, загружаем пользователей и SSL статус');
await loadUsers();
await loadSslStatus();
} else {
console.log('[VDS] Пользователь НЕ является редактором, пропускаем загрузку пользователей и SSL');
}
// Обновляем статистику каждые 5 секунд

View File

@@ -239,10 +239,27 @@ function getStatusText(status) {
}
function formatContent(content) {
// Простое форматирование контента
// Форматирование контента
if (!content) return '';
// Заменяем переносы строк на <br>
// Если контент уже содержит HTML теги (например, из RichTextEditor), обрабатываем его
if (/<[a-z][\s\S]*>/i.test(content)) {
// Преобразуем iframe с локальными видео-файлами обратно в тег video
// Quill может преобразовывать video в iframe, но для локальных файлов нужен тег video
content = content.replace(/<iframe([^>]*?)src=["']([^"']+)["']([^>]*?)><\/iframe>/gi, (match, attrs1, url, attrs2) => {
// Проверяем, является ли это видео-файл из нашей системы
if (url.includes('/api/uploads/media/') && url.includes('/file')) {
// Преобразуем в тег video для локальных видео-файлов
return `<video controls class="ql-video" style="max-width: 100%; width: 100%; height: auto; min-height: 400px; border-radius: 8px; margin: 1.5rem 0; display: block;" src="${url}"></video>`;
}
// Оставляем iframe для внешних видео (YouTube, Vimeo и т.д.)
return match;
});
return content;
}
// Иначе заменяем переносы строк на <br>
return content.replace(/\n/g, '<br>');
}
@@ -349,6 +366,8 @@ onMounted(() => {
border-radius: var(--radius-lg);
padding: 25px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
width: 100%;
max-width: 100%;
}
.page-content {
@@ -356,6 +375,8 @@ onMounted(() => {
border-radius: var(--radius-sm);
padding: 25px;
border: 1px solid #e9ecef;
width: 100%;
max-width: 100%;
}
.content-section {
@@ -364,6 +385,8 @@ onMounted(() => {
padding: 25px;
margin-bottom: 20px;
border: 1px solid #e9ecef;
width: 100%;
max-width: 100%;
}
.content-section:last-child {
@@ -387,6 +410,64 @@ onMounted(() => {
color: #333;
}
/* Стили для видео в контенте */
.main-content :deep(video) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
}
.main-content :deep(video.ql-video) {
width: 100%;
max-width: 100%;
min-height: 400px;
}
.main-content :deep(video:focus) {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Стили для iframe в контенте (для внешних видео) */
.main-content :deep(iframe) {
max-width: 100%;
width: 100%;
height: auto;
min-height: 400px;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #000;
border: none;
}
.main-content :deep(iframe.ql-video) {
min-height: 400px;
aspect-ratio: 16 / 9;
}
.main-content :deep(iframe:focus) {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Стили для изображений в контенте */
.main-content :deep(img) {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1.5rem 0;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.file-preview { display: flex; flex-direction: column; gap: 12px; }
.pdf-embed { width: 100%; height: 70vh; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }
.image-preview { max-width: 100%; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }