Добавил редактор статей, исправил комментарий, отредактировал отображение статей
This commit is contained in:
parent
2ba5793115
commit
0a2088e493
@ -89,6 +89,8 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
'django.contrib.sitemaps',
|
'django.contrib.sitemaps',
|
||||||
'blog.apps.BlogConfig',
|
'blog.apps.BlogConfig',
|
||||||
|
'taggit',
|
||||||
|
'django_ckeditor_5',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -221,4 +223,36 @@ SERVER_EMAIL = EMAIL_HOST_USER
|
|||||||
# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',')
|
# ADMIN_EMAILS = os.getenv('ADMIN_EMAILS', 'nikdizell@gmail.com').split(',')
|
||||||
ADMIN_EMAILS = os.getenv('ADMIN_EMAILS').split(',')
|
ADMIN_EMAILS = os.getenv('ADMIN_EMAILS').split(',')
|
||||||
|
|
||||||
|
# Путь для загрузки файлов редактором
|
||||||
|
CKEDITOR_5_UPLOAD_FILE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'bmp', 'webp'] # допустимые типы
|
||||||
|
CKEDITOR_5_UPLOAD_PATH = "uploads/ckeditor/" # папка внутри MEDIA_ROOT
|
||||||
|
CKEDITOR_5_CONFIGS = {
|
||||||
|
'default': {
|
||||||
|
'toolbar': {
|
||||||
|
'items': [
|
||||||
|
'heading', '|',
|
||||||
|
'bold', 'italic', 'underline', 'strikethrough', '|',
|
||||||
|
'alignment',
|
||||||
|
'link', 'bulletedList', 'numberedList', 'blockQuote', '|',
|
||||||
|
'code', 'codeBlock', 'insertTable', '|',
|
||||||
|
'imageUpload', 'mediaEmbed', '|',
|
||||||
|
'undo', 'redo',
|
||||||
|
'fontSize', 'fontFamily', 'highlight', 'horizontalLine', 'removeFormat', '|',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight'],
|
||||||
|
'styles': ['alignLeft', 'alignCenter', 'alignRight'],
|
||||||
|
},
|
||||||
|
'table': {
|
||||||
|
'contentToolbar': ['tableColumn', 'tableRow', 'mergeTableCells'],
|
||||||
|
},
|
||||||
|
'mediaEmbed': {
|
||||||
|
'previewsInData': True, # для встраивания видео (YouTube и др.)
|
||||||
|
},
|
||||||
|
'height': 500,
|
||||||
|
'width': '100%',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,6 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from OneCprogsite import settings
|
from OneCprogsite import settings
|
||||||
from programmer.views import *
|
from programmer.views import *
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
@ -26,6 +24,7 @@ urlpatterns = [
|
|||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include('programmer.urls')),
|
path('', include('programmer.urls')),
|
||||||
path('blog/', include('blog.urls')),
|
path('blog/', include('blog.urls')),
|
||||||
|
path('ckeditor5/', include('django_ckeditor_5.urls')),
|
||||||
# path('', index, name='home'),
|
# path('', index, name='home'),
|
||||||
# path('about/', about, name='about'),
|
# path('about/', about, name='about'),
|
||||||
# path('solution/', solution, name='solution'),
|
# path('solution/', solution, name='solution'),
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Category, Article, Comment
|
from .models import Category, Article, Comment
|
||||||
|
from django.db import models
|
||||||
|
from django_ckeditor_5.widgets import CKEditor5Widget
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Category)
|
@admin.register(Category)
|
||||||
class CategoryAdmin(admin.ModelAdmin):
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
@ -9,14 +14,47 @@ class CategoryAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Article)
|
@admin.register(Article)
|
||||||
class ArticleAdmin(admin.ModelAdmin):
|
class ArticleAdmin(admin.ModelAdmin):
|
||||||
list_display = ('title', 'category', 'author', 'is_published', 'views_count', 'time_create')
|
formfield_overrides = {
|
||||||
list_filter = ('is_published', 'category', 'time_create')
|
models.TextField: {'widget': CKEditor5Widget(config_name='default')},
|
||||||
search_fields = ('title', 'content')
|
}
|
||||||
|
list_display = ('title', 'category', 'author', 'is_published', 'views_count', 'time_create', 'tag_list')
|
||||||
|
list_filter = ('is_published', 'category', 'tags', 'time_create')
|
||||||
|
search_fields = ('title', 'content', 'tags__name')
|
||||||
prepopulated_fields = {'slug': ('title',)}
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
|
autocomplete_fields = ('author',) # вместо raw_id_fields для удобства
|
||||||
|
# filter_horizontal = ('tags',) # для удобного выбора тегов
|
||||||
raw_id_fields = ('author',) # удобно при большом количестве пользователей
|
raw_id_fields = ('author',) # удобно при большом количестве пользователей
|
||||||
date_hierarchy = 'time_create'
|
date_hierarchy = 'time_create'
|
||||||
actions = ['make_published', 'make_unpublished']
|
actions = ['make_published', 'make_unpublished']
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('title', 'slug', 'category', 'author', 'tags', 'is_published')
|
||||||
|
}),
|
||||||
|
('Содержание', {
|
||||||
|
'fields': ('content', 'image'),
|
||||||
|
'description': 'Основной текст статьи. Используйте редактор для форматирования.'
|
||||||
|
}),
|
||||||
|
('SEO', {
|
||||||
|
'fields': ('meta_description',),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
('Служебное', {
|
||||||
|
'fields': ('views_count',),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = ('views_count',) # только для просмотра
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).prefetch_related('tags')
|
||||||
|
|
||||||
|
def tag_list(self, obj):
|
||||||
|
return ", ".join(o.name for o in obj.tags.all())
|
||||||
|
|
||||||
|
tag_list.short_description = "Теги"
|
||||||
|
|
||||||
def make_published(self, request, queryset):
|
def make_published(self, request, queryset):
|
||||||
queryset.update(is_published=True)
|
queryset.update(is_published=True)
|
||||||
make_published.short_description = "Опубликовать выбранные статьи"
|
make_published.short_description = "Опубликовать выбранные статьи"
|
||||||
@ -25,6 +63,17 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
queryset.update(is_published=False)
|
queryset.update(is_published=False)
|
||||||
make_unpublished.short_description = "Снять с публикации"
|
make_unpublished.short_description = "Снять с публикации"
|
||||||
|
|
||||||
|
# Добавляем кнопку предпросмотра для черновиков
|
||||||
|
def view_on_site(self, obj):
|
||||||
|
if obj.is_published:
|
||||||
|
return obj.get_absolute_url()
|
||||||
|
else:
|
||||||
|
# ссылка на предпросмотр черновика
|
||||||
|
return reverse('blog:draft_preview', args=[obj.slug])
|
||||||
|
|
||||||
|
view_on_site.short_description = "Предпросмотр"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Comment)
|
@admin.register(Comment)
|
||||||
class CommentAdmin(admin.ModelAdmin):
|
class CommentAdmin(admin.ModelAdmin):
|
||||||
list_display = ('author_name', 'article', 'is_moderated', 'time_create')
|
list_display = ('author_name', 'article', 'is_moderated', 'time_create')
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from .models import Comment
|
from .models import Comment
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
|
||||||
class CommentForm(forms.ModelForm):
|
class CommentForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -8,6 +11,17 @@ class CommentForm(forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}),
|
'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}),
|
||||||
'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}),
|
'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}),
|
||||||
'content': forms.Textarea(attrs={'class': 'form-textarea', 'rows': 4, 'placeholder': 'Текст комментария'}),
|
'content': forms.Textarea(attrs={'class': 'form-textarea', 'rows': 4, 'placeholder': 'Текст комментария',
|
||||||
|
'autocomplete': 'off'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def clean_content(self):
|
||||||
|
content = self.cleaned_data['content']
|
||||||
|
# Удаляем HTML-теги и проверяем, остался ли текст
|
||||||
|
clean_text = strip_tags(content)
|
||||||
|
if not clean_text.strip():
|
||||||
|
raise ValidationError("Комментарий не должен состоять только из HTML-тегов")
|
||||||
|
# Если хотите запретить любые теги, можно сравнить content и clean_text
|
||||||
|
if content != clean_text:
|
||||||
|
raise ValidationError("HTML-теги в комментариях запрещены")
|
||||||
|
return content
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ class Category(models.Model):
|
|||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
"""Статья блога"""
|
"""Статья блога"""
|
||||||
|
tags = TaggableManager(blank=True, verbose_name="Теги")
|
||||||
title = models.CharField(max_length=200, verbose_name="Заголовок")
|
title = models.CharField(max_length=200, verbose_name="Заголовок")
|
||||||
slug = models.SlugField(max_length=220, unique=True, verbose_name="URL")
|
slug = models.SlugField(max_length=220, unique=True, verbose_name="URL")
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
@ -38,6 +40,12 @@ class Article(models.Model):
|
|||||||
is_published = models.BooleanField(default=True, verbose_name="Опубликовано")
|
is_published = models.BooleanField(default=True, verbose_name="Опубликовано")
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
time_update = models.DateTimeField(auto_now=True, verbose_name="Дата изменения")
|
time_update = models.DateTimeField(auto_now=True, verbose_name="Дата изменения")
|
||||||
|
meta_description = models.CharField(
|
||||||
|
max_length=160,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Meta-описание",
|
||||||
|
help_text="Краткое описание для поисковых систем (до 160 символов)"
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Статья"
|
verbose_name = "Статья"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
{% load django_bootstrap5 %}
|
{% load django_bootstrap5 %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
|
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -21,13 +21,13 @@
|
|||||||
<img src="{{ article.image.url }}" alt="{{ article.title }}" style="max-width: 100%; margin-bottom: 1rem;">
|
<img src="{{ article.image.url }}" alt="{{ article.title }}" style="max-width: 100%; margin-bottom: 1rem;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{ article.content|linebreaks }}
|
{{ article.content|safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Комментарии -->
|
<!-- Комментарии -->
|
||||||
<h3>Комментарии</h3>
|
<h3>Комментарии</h3>
|
||||||
{% for comment in article.comments.all %}
|
{% for comment in moderated_comments %}
|
||||||
<div class="modern-card" style="margin-bottom: 1rem;">
|
<div class="modern-card" style="margin-bottom: 1rem;">
|
||||||
<p><strong>{{ comment.author_name }}</strong> ({{ comment.time_create|date:"d.m.Y H:i" }})</p>
|
<p><strong>{{ comment.author_name }}</strong> ({{ comment.time_create|date:"d.m.Y H:i" }})</p>
|
||||||
<p>{{ comment.content|linebreaks }}</p>
|
<p>{{ comment.content|linebreaks }}</p>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{% extends 'programmer/base.html' %}
|
{% extends 'programmer/base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load blog_extras %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
|
<link rel="stylesheet" href="{% static 'programmer/css/recall.css' %}">
|
||||||
@ -23,7 +24,7 @@
|
|||||||
{% for article in articles %}
|
{% for article in articles %}
|
||||||
<div class="modern-card">
|
<div class="modern-card">
|
||||||
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
|
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
|
||||||
<p>{{ article.content|truncatewords:30 }}</p>
|
<p>{{ article.content|clean_html|truncatewords:30 }}</p>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>📅 {{ article.time_create|date:"d.m.Y" }}</span>
|
<span>📅 {{ article.time_create|date:"d.m.Y" }}</span>
|
||||||
<span>👁 {{ article.views_count }}</span>
|
<span>👁 {{ article.views_count }}</span>
|
||||||
|
|||||||
0
blog/templatetags/__init__.py
Normal file
0
blog/templatetags/__init__.py
Normal file
14
blog/templatetags/blog_extras.py
Normal file
14
blog/templatetags/blog_extras.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django import template
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
import html
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def clean_html(value):
|
||||||
|
"""Удаляет HTML-теги и заменяет сущности на символы."""
|
||||||
|
# Декодируем HTML-сущности (например, → пробел)
|
||||||
|
value = html.unescape(value)
|
||||||
|
# Удаляем все HTML-теги
|
||||||
|
return strip_tags(value)
|
||||||
@ -7,5 +7,6 @@ urlpatterns = [
|
|||||||
path('', views.ArticleListView.as_view(), name='article_list'),
|
path('', views.ArticleListView.as_view(), name='article_list'),
|
||||||
path('category/<slug:category_slug>/', views.ArticleListView.as_view(), name='category_detail'),
|
path('category/<slug:category_slug>/', views.ArticleListView.as_view(), name='category_detail'),
|
||||||
path('<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
|
path('<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
|
||||||
|
path('draft/<slug:slug>/', views.ArticleDraftPreviewView.as_view(), name='draft_preview'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from .services import (
|
|||||||
from .forms import CommentForm
|
from .forms import CommentForm
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
|
|
||||||
|
|
||||||
class ArticleListView(MenuContextMixin, ListView):
|
class ArticleListView(MenuContextMixin, ListView):
|
||||||
@ -66,6 +67,12 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
|
|||||||
else:
|
else:
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""Возвращает пустые начальные данные для формы комментария."""
|
||||||
|
initial = super().get_initial()
|
||||||
|
initial['content'] = ''
|
||||||
|
return initial
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
comment = add_comment_to_article(
|
comment = add_comment_to_article(
|
||||||
article=self.object,
|
article=self.object,
|
||||||
@ -74,4 +81,19 @@ class ArticleDetailView(MenuContextMixin, DetailView, CreateView):
|
|||||||
messages.success(self.request, "Ваш комментарий отправлен на модерацию.")
|
messages.success(self.request, "Ваш комментарий отправлен на модерацию.")
|
||||||
return redirect(self.object.get_absolute_url())
|
return redirect(self.object.get_absolute_url())
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
# Добавляем только одобренные комментарии
|
||||||
|
context['moderated_comments'] = self.object.comments.filter(is_moderated=True)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(staff_member_required, name='dispatch')
|
||||||
|
class ArticleDraftPreviewView(DetailView):
|
||||||
|
model = Article
|
||||||
|
template_name = 'blog/article_detail.html' # используем тот же шаблон
|
||||||
|
context_object_name = 'article'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Для предпросмотра показываем даже неопубликованные статьи
|
||||||
|
return Article.objects.all() # без фильтрации по is_published
|
||||||
@ -21,7 +21,7 @@ class MenuContextMixin(ContextMixin):
|
|||||||
menu = [
|
menu = [
|
||||||
{'title': "Главная", 'url_name': 'home'},
|
{'title': "Главная", 'url_name': 'home'},
|
||||||
{'title': "Проекты", 'url_name': 'solution'},
|
{'title': "Проекты", 'url_name': 'solution'},
|
||||||
# {'title': "Статьи", 'url_name': 'blog'},
|
{'title': "Статьи", 'url_name': 'blog'},
|
||||||
{'title': "Отзывы", 'url_name': 'recall'},
|
{'title': "Отзывы", 'url_name': 'recall'},
|
||||||
{'title': "Обо мне", 'url_name': 'about'}
|
{'title': "Обо мне", 'url_name': 'about'}
|
||||||
]
|
]
|
||||||
|
|||||||
139
programmer/static/blog/css/article_content.css
Normal file
139
programmer/static/blog/css/article_content.css
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/* Основная типографика */
|
||||||
|
.article-content {
|
||||||
|
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 800px; /* при необходимости */
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h1,
|
||||||
|
.article-content h2,
|
||||||
|
.article-content h3,
|
||||||
|
.article-content h4,
|
||||||
|
.article-content h5,
|
||||||
|
.article-content h6 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content p {
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content blockquote {
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
padding-left: 1em;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content code {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таблицы */
|
||||||
|
.article-content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content th,
|
||||||
|
.article-content td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content th {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Изображения */
|
||||||
|
.article-content figure.image {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content figure.image img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Выравнивание изображений (классы, используемые CKEditor 5) */
|
||||||
|
.article-content .image-style-align-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .image-style-align-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .image-style-align-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Подписи к изображениям (если используются) */
|
||||||
|
.article-content figure.image figcaption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Встроенные медиа (видео) */
|
||||||
|
.article-content .media {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 56.25%; /* 16:9 */
|
||||||
|
height: 0;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .media iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Списки */
|
||||||
|
.article-content ul,
|
||||||
|
.article-content ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Горизонтальная линия */
|
||||||
|
.article-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #eee;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
@ -160,6 +160,14 @@
|
|||||||
|
|
||||||
/* Адаптивность для мобильных устройств */
|
/* Адаптивность для мобильных устройств */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.content-card .image-style-align-left,
|
||||||
|
.content-card .image-style-align-right {
|
||||||
|
float: none !important;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
.recall-content {
|
.recall-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
@ -226,3 +234,21 @@
|
|||||||
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-card img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card figure.image {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card figure.image img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
@ -867,7 +867,25 @@ body {
|
|||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
/* overflow: visible !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card figure.image {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card figure.image img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-card::before {
|
.content-card::before {
|
||||||
@ -1298,6 +1316,14 @@ body {
|
|||||||
.breadcrumbs {
|
.breadcrumbs {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-card .image-style-align-left,
|
||||||
|
.content-card .image-style-align-right {
|
||||||
|
float: none !important;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-right: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
139
static/blog/css/article_content.css
Normal file
139
static/blog/css/article_content.css
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/* Основная типографика */
|
||||||
|
.article-content {
|
||||||
|
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 800px; /* при необходимости */
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content h1,
|
||||||
|
.article-content h2,
|
||||||
|
.article-content h3,
|
||||||
|
.article-content h4,
|
||||||
|
.article-content h5,
|
||||||
|
.article-content h6 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content p {
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content blockquote {
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
padding-left: 1em;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content code {
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таблицы */
|
||||||
|
.article-content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content th,
|
||||||
|
.article-content td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content th {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Изображения */
|
||||||
|
.article-content figure.image {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 1em 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content figure.image img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Выравнивание изображений (классы, используемые CKEditor 5) */
|
||||||
|
.article-content .image-style-align-left {
|
||||||
|
float: left;
|
||||||
|
margin-right: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .image-style-align-right {
|
||||||
|
float: right;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .image-style-align-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Подписи к изображениям (если используются) */
|
||||||
|
.article-content figure.image figcaption {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Встроенные медиа (видео) */
|
||||||
|
.article-content .media {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 56.25%; /* 16:9 */
|
||||||
|
height: 0;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content .media iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Списки */
|
||||||
|
.article-content ul,
|
||||||
|
.article-content ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Горизонтальная линия */
|
||||||
|
.article-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #eee;
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user