正式版 #17

Closed
prcsjxomk wants to merge 5 commits from develop into master

Binary file not shown.

@ -0,0 +1,73 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Article, Category, Tag, ArticleImage
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
# 关联临时图片到新创建的文章
# 获取会话中的临时图片ID列表
temp_image_ids = self.request.session.get('temp_image_ids', [])
# 查找这些临时图片
temp_images = ArticleImage.objects.filter(id__in=temp_image_ids)
# 将这些图片关联到新创建的文章
for image in temp_images:
image.article = form.instance
image.save()
# 清除会话中的临时图片ID列表
self.request.session['temp_image_ids'] = []
self.request.session.modified = True
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})

@ -0,0 +1,46 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy
from .models import Article, Category, Tag, ArticleImage
class ArticleDeleteView(LoginRequiredMixin, DeleteView):
"""删除文章视图"""
model = Article
template_name = 'blog/article_delete_confirm.html'
pk_url_kwarg = 'article_id'
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以删除
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def delete(self, request, *args, **kwargs):
# 删除文章前先删除所有相关图片
self.object = self.get_object()
images = ArticleImage.objects.filter(article=self.object)
for image in images:
# 删除图片文件
image.image.delete()
# 删除图片记录
image.delete()
# 调用父类的delete方法删除文章
return super().delete(request, *args, *kwargs)
def get_success_url(self):
return reverse_lazy('blog:index')

@ -0,0 +1,101 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import UpdateView
from django.urls import reverse_lazy
from .models import Article, Category, Tag, ArticleImage
from .forms import ArticleForm
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def get_initial(self):
initial = super().get_initial()
# 设置分类初始值
if self.object.category:
initial['category'] = self.object.category.name
# 设置标签初始值
if self.object.tags.exists():
tag_names = [tag.name for tag in self.object.tags.all()]
initial['tags'] = ', '.join(tag_names)
return initial
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
form.instance.tags.clear() # 清除现有标签
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
else:
form.instance.tags.clear() # 如果没有标签,清除所有标签
# 关联临时图片到文章
# 获取会话中的临时图片ID列表
temp_image_ids = self.request.session.get('temp_image_ids', [])
# 查找这些临时图片
temp_images = ArticleImage.objects.filter(id__in=temp_image_ids)
# 将这些图片关联到文章
for image in temp_images:
image.article = form.instance
image.save()
# 清除会话中的临时图片ID列表
self.request.session['temp_image_ids'] = []
self.request.session.modified = True
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})

@ -0,0 +1,135 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Article, Category, Tag, ArticleImage
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def get_initial(self):
initial = super().get_initial()
# 设置分类初始值
if self.object.category:
initial['category'] = self.object.category.name
# 设置标签初始值
if self.object.tags.exists():
tag_names = [tag.name for tag in self.object.tags.all()]
initial['tags'] = ', '.join(tag_names)
return initial
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
form.instance.tags.clear() # 清除现有标签
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
else:
form.instance.tags.clear() # 如果没有标签,清除所有标签
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})

@ -0,0 +1,153 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect, get_object_or_404
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .models import Article, Category, Tag, ArticleImage
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
# 处理临时上传的图片
temp_image_ids = self.request.session.get('temp_image_ids', [])
if temp_image_ids:
# 关联这些图片到当前文章
ArticleImage.objects.filter(id__in=temp_image_ids).update(article=self.object)
# 清除会话中的临时图片ID列表
self.request.session['temp_image_ids'] = []
self.request.session.modified = True
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['all_categories'] = Category.objects.all()
context['all_tags'] = Tag.objects.all()
return context
def get_initial(self):
initial = super().get_initial()
# 设置分类初始值
if self.object.category:
initial['category'] = self.object.category.name
# 设置标签初始值
if self.object.tags.exists():
tag_names = [tag.name for tag in self.object.tags.all()]
initial['tags'] = ', '.join(tag_names)
return initial
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
# 先不保存表单,先处理分类
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
# 直接设置分类对象,而不是名称
form.instance.category = category
# 先保存文章实例,以便可以添加多对多关系
# 但在保存前先从表单中移除category字段避免表单尝试保存它
category_temp = form.cleaned_data.pop('category')
tags_temp = form.cleaned_data.pop('tags')
response = super().form_valid(form)
# 处理标签
if tags_temp:
tag_names = [tag.strip() for tag in tags_temp.split(',') if tag.strip()]
form.instance.tags.clear() # 清除现有标签
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
else:
form.instance.tags.clear() # 如果没有标签,清除所有标签
# 处理临时上传的图片
temp_image_ids = self.request.session.get('temp_image_ids', [])
if temp_image_ids:
# 关联这些图片到当前文章
ArticleImage.objects.filter(id__in=temp_image_ids).update(article=self.object)
# 清除会话中的临时图片ID列表
self.request.session['temp_image_ids'] = []
self.request.session.modified = True
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})

@ -1,7 +1,9 @@
import logging
from django import forms
from django.utils.translation import gettext_lazy as _
from haystack.forms import SearchForm
from .models import Article, Category, Tag
logger = logging.getLogger(__name__)
@ -17,3 +19,62 @@ class BlogSearchForm(SearchForm):
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas
class ArticleForm(forms.ModelForm):
"""文章表单"""
title = forms.CharField(label='标题', max_length=200, required=True)
body = forms.CharField(label='内容', widget=forms.Textarea(attrs={'id': 'id_body_md'}), required=True)
status = forms.ChoiceField(
label='状态',
choices=[('p', '发布'), ('d', '草稿')],
initial='p',
widget=forms.Select,
required=True
)
comment_status = forms.ChoiceField(
label='评论状态',
choices=[('o', '开启'), ('c', '关闭')],
initial='o',
widget=forms.Select,
required=True
)
type = forms.ChoiceField(
label='类型',
choices=[('a', '文章'), ('p', '页面')],
initial='a',
widget=forms.Select,
required=True
)
show_toc = forms.BooleanField(label='显示目录', required=False)
category = forms.CharField(
label='分类',
required=True,
widget=forms.TextInput(attrs={
'class': 'form-control',
'list': 'category-list',
'placeholder': '选择或输入分类名称'
})
)
tags = forms.CharField(
label='标签',
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'list': 'tag-list',
'placeholder': '选择或输入标签,多个标签用逗号分隔'
})
)
class Meta:
model = Article
fields = ['title', 'body', 'status', 'comment_status', 'type', 'show_toc']
def __init__(self, *args, **kwargs):
super(ArticleForm, self).__init__(*args, **kwargs)
self.fields['title'].widget.attrs.update({'class': 'form-control'})
self.fields['body'].widget.attrs.update({'class': 'form-control mdeditor', 'rows': 20})
self.fields['status'].widget.attrs.update({'class': 'form-control'})
self.fields['comment_status'].widget.attrs.update({'class': 'form-control'})
self.fields['type'].widget.attrs.update({'class': 'form-control'})
self.fields['show_toc'].widget.attrs.update({'class': 'form-check-input'})

@ -0,0 +1,15 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import ArticleImage
class ImageUploadForm(forms.ModelForm):
"""图片上传表单"""
class Meta:
model = ArticleImage
fields = ['image', 'description']
widgets = {
'image': forms.FileInput(attrs={'class': 'form-control', 'accept': 'image/*'}),
'description': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('可选,添加图片描述')})
}

@ -0,0 +1,127 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.generic.edit import CreateView, DeleteView
from django.urls import reverse_lazy
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Article, ArticleImage
from .forms_image import ImageUploadForm
class ImageUploadView(LoginRequiredMixin, CreateView):
"""上传图片视图"""
model = ArticleImage
form_class = ImageUploadForm
template_name = 'blog/image_upload.html'
def form_valid(self, form):
article_id = self.kwargs.get('article_id')
article = get_object_or_404(Article, id=article_id)
# 检查用户是否有权限上传图片
if article.author != self.request.user and not self.request.user.is_superuser:
return JsonResponse({'status': 'error', 'message': '没有权限上传图片'}, status=403)
form.instance.article = article
self.object = form.save()
# 返回JSON响应包含图片URL和ID
return JsonResponse({
'status': 'success',
'image_id': self.object.id,
'image_url': self.object.image.url,
'description': self.object.description
})
def form_invalid(self, form):
return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)
@login_required
@require_POST
@csrf_exempt
def markdown_image_upload(request):
"""为Markdown编辑器提供的图片上传接口"""
if request.method == 'POST' and request.FILES.get('image'):
image_file = request.FILES['image']
description = request.POST.get('description', '')
article_id = request.POST.get('article_id')
try:
# 获取会话中的临时图片ID列表
temp_image_ids = request.session.get('temp_image_ids', [])
# 创建图片对象,但不立即关联到文章
temp_image = ArticleImage(
image=image_file,
description=description or "Markdown编辑器上传"
)
# 如果有文章ID尝试关联到文章
if article_id:
try:
from blog.models import Article
article = Article.objects.get(id=article_id, author=request.user)
temp_image.article = article
except Article.DoesNotExist:
# 文章不存在,不关联
temp_image.article_id = None
else:
# 没有文章ID保存为临时图片
temp_image.article_id = None
# 保存图片
temp_image.save()
# 如果是临时图片将图片ID添加到会话中的临时图片列表
if not temp_image.article:
temp_image_ids.append(temp_image.id)
request.session['temp_image_ids'] = temp_image_ids
request.session.modified = True
# 返回Markdown编辑器需要的格式
return JsonResponse({
'success': 1,
'message': '上传成功',
'url': temp_image.image.url,
'image_id': temp_image.id # 添加图片ID以便后续可以删除
})
except Exception as e:
# 记录错误并返回失败信息
import logging
logger = logging.getLogger(__name__)
logger.error(f"图片上传失败: {str(e)}")
return JsonResponse({
'success': 0,
'message': f'上传失败: {str(e)}'
})
return JsonResponse({
'success': 0,
'message': '上传失败:未提供图片文件'
})
class ImageDeleteView(LoginRequiredMixin, DeleteView):
"""删除图片视图"""
model = ArticleImage
pk_url_kwarg = 'image_id'
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# 检查用户是否有权限删除图片
if self.object.article.author != request.user and not request.user.is_superuser:
return JsonResponse({'status': 'error', 'message': '没有权限删除图片'}, status=403)
# 删除图片文件
self.object.image.delete()
self.object.delete()
return JsonResponse({'status': 'success', 'message': '图片已删除'})

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from blog.models import Category
class Command(BaseCommand):
help = '创建文章分类'
def handle(self, *args, **options):
categories = [
'技术',
'生活',
'旅行',
'美食',
'摄影',
'读书',
'电影',
'音乐',
'编程',
'设计',
'健康',
'教育',
'职场',
'财经',
'历史',
'文化',
'体育',
'游戏',
'科技'
]
for name in categories:
category, created = Category.objects.get_or_create(
name=name,
defaults={'parent_category': None}
)
if created:
self.stdout.write(self.style.SUCCESS(f'创建分类: {name}'))
else:
self.stdout.write(self.style.WARNING(f'分类已存在: {name}'))
self.stdout.write(self.style.SUCCESS('分类创建完成'))

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Category
class Command(BaseCommand):
help = '列出所有文章分类'
def handle(self, *args, **options):
categories = Category.objects.all()
self.stdout.write('当前数据库中的分类:')
for category in categories:
self.stdout.write(f'- {category.name} (ID: {category.id})')
self.stdout.write(f'总计: {categories.count()} 个分类')

@ -0,0 +1,24 @@
# Generated by Django
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='articleimage',
name='article',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=models.deletion.CASCADE,
related_name='images',
to='blog.article'
),
),
]

@ -0,0 +1,29 @@
# Generated by Django 4.2.14 on 2025-11-20 20:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
]
operations = [
migrations.CreateModel(
name='ArticleImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='article_images/%Y/%m/%d/', verbose_name='图片')),
('description', models.CharField(blank=True, max_length=255, verbose_name='图片描述')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='blog.article')),
],
options={
'verbose_name': '文章图片',
'verbose_name_plural': '文章图片',
'ordering': ['-created_time'],
},
),
]

@ -0,0 +1,14 @@
# Generated by Django 4.2.14 on 2025-11-20 22:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_auto_20231201_1200'),
('blog', '0007_articleimage'),
]
operations = [
]

@ -304,6 +304,22 @@ class SideBar(models.Model):
return self.name
class ArticleImage(models.Model):
"""文章图片模型"""
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='images', null=True, blank=True)
image = models.ImageField(_('图片'), upload_to='images/')
description = models.CharField(_('图片描述'), max_length=255, blank=True)
created_time = models.DateTimeField(_('创建时间'), auto_now_add=True)
class Meta:
verbose_name = _('文章图片')
verbose_name_plural = verbose_name
ordering = ['-created_time']
def __str__(self):
return f"{self.article.title} - {self.image.name}"
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(

@ -0,0 +1,91 @@
/* 自定义样式 - 修改导航栏背景色和按钮样式 */
/* 导航栏背景色改为天蓝色 */
.main-navigation {
background: linear-gradient(to right, #2e9ac5, #66b8db) !important;
}
/* 文章标题链接样式 - 去掉蓝色按钮效果 */
.entry-title a {
color: #2c3e50 !important;
text-decoration: none !important;
background: none !important;
padding: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.entry-title a:hover {
color: #3498db !important;
text-decoration: underline !important;
background: none !important;
}
/* 侧边栏链接样式 - 去掉蓝色按钮效果 */
.widget a {
color: #34495e !important;
text-decoration: none !important;
background: none !important;
padding: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
}
.widget a:hover {
color: #3498db !important;
text-decoration: underline !important;
background: none !important;
}
/* 文章阅读更多链接 - 去掉蓝色按钮效果 */
.read-more {
background: none !important;
color: #3498db !important;
padding: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
text-decoration: underline !important;
}
.read-more:hover {
background: none !important;
color: #2980b9 !important;
transform: none !important;
box-shadow: none !important;
}
/* 文章链接 - 去掉蓝色按钮效果 */
a[href*="/p/"],
a[href*="/article/"] {
background: none !important;
color: #3498db !important;
padding: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
text-decoration: underline !important;
margin: 0 !important;
}
a[href*="/p/"]:hover,
a[href*="/article/"]:hover {
background: none !important;
color: #2980b9 !important;
transform: none !important;
box-shadow: none !important;
}
/* 背景图片样式 */
body {
background-color: #f8f9fa;
background-image: url('/static/blog/img/default_bg.jpg');
background-size: cover;
background-position: center;
background-attachment: fixed;
background-repeat: no-repeat;
}
/* 网站容器样式,确保背景图片可见 */
.site {
background-color: rgba(255, 255, 255, 0.9);
}

@ -0,0 +1,90 @@
/* 简单的图标样式 */
/* 图标基础样式 */
[class^="icon-"], [class*=" icon-"] {
display: inline-block;
width: 1em;
height: 1em;
margin-right: 0.2em;
line-height: 1;
font-style: normal;
font-weight: normal;
text-align: center;
text-decoration: inherit;
text-transform: none;
speak: none;
}
/* 文件夹图标 */
.icon-folder:before {
content: "📁";
}
/* 标签图标 */
.icon-tag:before {
content: "🏷️";
}
/* 评论图标 */
.icon-comment:before {
content: "💬";
}
/* 搜索图标 */
#searchform input[type="submit"]:before {
content: "🔍";
margin-right: 0.25em;
}
/* 首页图标 */
#menu-item-3498 a:before {
content: "🏠";
margin-right: 0.25em;
}
/* 归档图标 */
.menu-item a[href*="archives"]:before {
content: "📚";
margin-right: 0.25em;
}
/* 管理站点图标 */
.menu-item a[href*="admin"]:before {
content: "⚙️";
margin-right: 0.25em;
}
/* 登录/登出图标 */
.menu-item a[href*="login"]:before {
content: "🔑";
margin-right: 0.25em;
}
.menu-item a[href*="logout"]:before {
content: "🚪";
margin-right: 0.25em;
}
/* 发布文章图标 */
.menu-item a[href*="create"]:before {
content: "✍️";
margin-right: 0.25em;
}
/* 我的文章图标 */
.menu-item a[href*="my_articles"]:before {
content: "📝";
margin-right: 0.25em;
}
/* 返回顶部按钮 */
#rocket:before {
content: "⬆️";
font-size: 1.5rem;
}
/* 背景切换按钮 */
.bg-toggle-btn i {
font-size: 1.5rem;
}

@ -0,0 +1,968 @@
/* 现代化博客样式 - 保持原有布局但提升视觉效果 */
/* 全局样式优化 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #333;
line-height: 1.6;
background-color: #f8f9fa;
}
/* 网站容器优化 */
.site {
max-width: 1200px;
margin: 2rem auto;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
/* 网站头部优化 */
.site-header {
background: linear-gradient(to bottom, #fff, #f8f9fa);
padding: 1.5rem 0;
border-bottom: 1px solid #eee;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.site-title a {
font-size: 2.2rem;
font-weight: 700;
color: #2c3e50;
text-decoration: none;
transition: color 0.3s ease;
display: inline-block;
position: relative;
}
.site-title a::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 0;
height: 3px;
background: linear-gradient(to right, #3498db, #2980b9);
transition: width 0.3s ease;
}
.site-title a:hover {
color: #3498db;
}
.site-title a:hover::after {
width: 100%;
}
.site-description {
color: #7f8c8d;
font-size: 1.1rem;
margin-top: 0.5rem;
font-style: italic;
}
/* 导航栏优化 */
.main-navigation {
margin-top: 1rem;
background: linear-gradient(to right, #34495e, #2c3e50);
padding: 0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
position: relative;
overflow: hidden;
}
.main-navigation::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, #3498db, #2980b9);
box-shadow: 0 2px 4px rgba(52, 152, 219, 0.3);
}
.main-navigation::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(to right, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
}
.main-navigation ul {
display: flex;
justify-content: center;
list-style: none;
margin: 0;
padding: 0;
}
.main-navigation li {
position: relative;
margin: 0;
}
.main-navigation a {
color: #ecf0f1;
text-decoration: none;
font-weight: 500;
padding: 0.9rem 1.2rem;
display: block;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.main-navigation a::before {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 3px;
background: linear-gradient(to right, #3498db, #2980b9);
transform: translateX(-50%);
transition: width 0.3s ease;
}
.main-navigation a:hover::before {
width: 80%;
}
.main-navigation a::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
}
.main-navigation a:hover::after {
width: 100px;
height: 100px;
}
.main-navigation a:hover {
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
/* 当前活动菜单项 */
.main-navigation .current-menu-item > a,
.main-navigation .current_page_item > a {
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.main-navigation .current-menu-item > a::before,
.main-navigation .current_page_item > a::before {
width: 80%;
}
/* 下拉菜单样式 */
.main-navigation .sub-menu {
position: absolute;
top: 100%;
left: 0;
background: linear-gradient(to bottom, #34495e, #2c3e50);
min-width: 220px;
opacity: 0;
visibility: hidden;
transform: translateY(15px);
transition: all 0.3s ease;
z-index: 1000;
border-radius: 0 0 6px 6px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.main-navigation li:hover > .sub-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.main-navigation .sub-menu li {
width: 100%;
position: relative;
}
.main-navigation .sub-menu a {
padding: 0.8rem 1.2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.main-navigation .sub-menu li:last-child a {
border-bottom: none;
}
.main-navigation .sub-menu a::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(to bottom, #3498db, #2980b9);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.main-navigation .sub-menu a:hover::before {
transform: scaleY(1);
}
/* 移动端菜单按钮 */
.menu-toggle {
display: none;
background-color: #34495e;
color: #ecf0f1;
padding: 0.75rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
margin: 1rem auto;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.menu-toggle::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s ease;
}
.menu-toggle:hover::before {
left: 100%;
}
.menu-toggle:hover {
background-color: #3498db;
}
/* 主内容区域优化 */
#main {
display: flex;
flex-wrap: wrap;
padding: 2rem;
}
#primary {
flex: 2;
padding-right: 2rem;
}
#secondary {
flex: 1;
}
/* 文章卡片样式 */
article.post {
margin-bottom: 2rem;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
article.post:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.entry-header {
padding: 1.5rem 1.5rem 0;
}
/* 文章顶部元信息 */
.article-meta-top {
display: flex;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #eee;
}
.author-avatar {
margin-right: 1rem;
}
.author-avatar img {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.author-info {
flex: 1;
}
.author-name {
font-weight: 600;
color: #2c3e50;
display: block;
}
.post-date {
font-size: 0.9rem;
color: #7f8c8d;
}
.post-views {
font-size: 0.9rem;
color: #7f8c8d;
}
/* 文章标题 */
.entry-title {
font-size: 1.5rem;
font-weight: 600;
line-height: 1.3;
margin-bottom: 1rem;
}
.pin-indicator {
color: #e74c3c;
font-weight: 600;
}
.entry-title a {
color: #2c3e50;
text-decoration: none;
transition: color 0.3s ease;
}
.entry-title a:hover {
color: #3498db;
}
/* 面包屑导航 */
.breadcrumb {
margin-bottom: 1rem;
font-size: 0.9rem;
color: #7f8c8d;
}
.breadcrumb a {
color: #3498db;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
/* 文章元信息 */
.article-meta {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #7f8c8d;
}
.article-meta span {
margin-right: 1rem;
margin-bottom: 0.5rem;
}
.article-meta a {
color: #3498db;
text-decoration: none;
}
.article-meta a:hover {
text-decoration: underline;
}
/* 文章内容 */
.entry-content {
padding: 0 1.5rem 1.5rem;
font-size: 1rem;
line-height: 1.7;
}
.article-summary {
margin-bottom: 1rem;
}
.read-more-container {
text-align: right;
margin-top: 1.5rem;
}
.read-more {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(to right, #3498db, #2980b9);
color: #fff;
padding: 0.6rem 1.2rem;
border-radius: 30px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3);
position: relative;
overflow: hidden;
}
.read-more::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s ease;
}
.read-more:hover::before {
left: 100%;
}
.read-more:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.4);
}
.read-more .arrow {
margin-left: 0.5rem;
font-weight: bold;
transition: transform 0.3s ease;
}
.read-more:hover .arrow {
transform: translateX(3px);
}
/* 统一所有阅读更多链接的样式 */
a[href*="/p/"],
a[href*="/article/"] {
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(to right, #3498db, #2980b9);
color: #fff;
padding: 0.6rem 1.2rem;
border-radius: 30px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3);
position: relative;
overflow: hidden;
margin: 0.5rem 0;
}
a[href*="/p/"]::before,
a[href*="/article/"]::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s ease;
}
a[href*="/p/"]:hover::before,
a[href*="/article/"]:hover::before {
left: 100%;
}
a[href*="/p/"]:hover,
a[href*="/article/"]:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.4);
color: #fff;
}
/* 目录样式 */
.toc-container {
background-color: #f8f9fa;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1.5rem;
}
.toc-title {
font-size: 1.1rem;
font-weight: 600;
margin-top: 0;
margin-bottom: 0.75rem;
color: #2c3e50;
}
.toc-content ul {
padding-left: 1.5rem;
margin: 0;
}
.toc-content li {
margin-bottom: 0.25rem;
}
.toc-content a {
color: #34495e;
text-decoration: none;
}
.toc-content a:hover {
color: #3498db;
}
/* 文章内容区域 */
.article-content {
margin-top: 1.5rem;
}
/* 文章页脚 */
.entry-footer {
padding: 0 1.5rem 1.5rem;
border-top: 1px solid #eee;
margin-top: 1.5rem;
}
/* 侧边栏样式 */
.widget-area {
padding-left: 1rem;
}
.widget {
background: #fff;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.widget-title {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #3498db;
}
.widget ul {
list-style: none;
padding: 0;
margin: 0;
}
.widget li {
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.widget li:last-child {
border-bottom: none;
}
.widget a {
color: #34495e;
text-decoration: none;
transition: color 0.3s ease;
}
.widget a:hover {
color: #3498db;
}
/* 搜索框样式 */
#searchform {
display: flex;
}
#searchform input[type="text"] {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 0.9rem;
}
#searchform input[type="submit"] {
background-color: #3498db;
color: #fff;
border: none;
padding: 0 1rem;
border-radius: 0 4px 4px 0;
cursor: pointer;
transition: background-color 0.3s ease;
}
#searchform input[type="submit"]:hover {
background-color: #2980b9;
}
/* 标签云样式 */
.tagcloud a {
display: inline-block;
margin: 0.25rem;
padding: 0.25rem 0.5rem;
background-color: #f1f2f6;
color: #34495e;
border-radius: 4px;
text-decoration: none;
transition: all 0.3s ease;
}
.tagcloud a:hover {
background-color: #3498db;
color: #fff;
}
/* 评论区域样式 */
.comments-area {
margin-top: 2rem;
padding: 1.5rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.comment-list {
padding: 0;
list-style: none;
}
.comment {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #eee;
}
.comment:last-child {
border-bottom: none;
}
.comment-author {
font-weight: 600;
color: #2c3e50;
}
.comment-meta {
font-size: 0.8rem;
color: #7f8c8d;
margin-bottom: 0.5rem;
}
/* 页脚样式 */
footer[role="contentinfo"] {
background-color: #34495e;
color: #ecf0f1;
padding: 2rem;
text-align: center;
margin-top: 2rem;
}
footer[role="contentinfo"] a {
color: #3498db;
text-decoration: none;
transition: color 0.3s ease;
}
footer[role="contentinfo"] a:hover {
color: #5dade2;
}
/* 归档页面样式 */
.archive-header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.archive-title {
font-size: 2rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.archive-title .icon-folder {
margin-right: 0.5rem;
}
.archive-description {
font-size: 1rem;
color: #7f8c8d;
}
.archive-content {
padding: 1rem 0;
}
/* 时间轴样式 */
.timeline-container {
position: relative;
padding: 1rem 0;
}
.timeline-container::before {
content: '';
position: absolute;
left: 30px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(to bottom, #3498db, #2980b9);
border-radius: 3px;
}
.timeline-year {
margin-bottom: 2rem;
}
.year-title {
position: relative;
display: flex;
align-items: center;
margin-bottom: 1.5rem;
padding-left: 60px;
}
.year-badge {
position: absolute;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background-color: #3498db;
color: #fff;
font-size: 1.2rem;
font-weight: 600;
border-radius: 50%;
box-shadow: 0 3px 10px rgba(52, 152, 219, 0.3);
z-index: 1;
}
.year-text {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
margin-right: 0.5rem;
}
.article-count {
font-size: 0.9rem;
color: #7f8c8d;
background-color: #f8f9fa;
padding: 0.2rem 0.5rem;
border-radius: 12px;
}
.timeline-months {
padding-left: 60px;
}
.timeline-month {
margin-bottom: 1.5rem;
}
.month-title {
position: relative;
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.month-badge {
position: absolute;
left: -45px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: #2ecc71;
color: #fff;
font-size: 0.9rem;
font-weight: 600;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(46, 204, 113, 0.3);
z-index: 1;
}
.month-text {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
margin-right: 0.5rem;
}
.timeline-articles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.archive-article {
display: flex;
background: #fff;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.archive-article:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.article-date {
margin-right: 1rem;
}
.day {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: #f1f2f6;
color: #2c3e50;
font-size: 1.1rem;
font-weight: 600;
border-radius: 50%;
}
.article-content {
flex: 1;
}
.article-title {
margin: 0 0 0.5rem;
font-size: 1rem;
font-weight: 600;
}
.article-title a {
color: #2c3e50;
text-decoration: none;
transition: color 0.3s ease;
}
.article-title a:hover {
color: #3498db;
}
.article-meta {
display: flex;
font-size: 0.8rem;
color: #7f8c8d;
}
.article-meta .category {
margin-right: 1rem;
}
.article-meta a {
color: #3498db;
text-decoration: none;
}
.article-meta a:hover {
text-decoration: underline;
}
/* 返回顶部按钮 */
#rocket {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 3rem;
height: 3rem;
background-color: #3498db;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
opacity: 0;
visibility: hidden;
}
#rocket.show {
opacity: 1;
visibility: visible;
}
#rocket:hover {
background-color: #2980b9;
transform: translateY(-5px);
}
#rocket::before {
content: "↑";
font-size: 1.5rem;
}
/* 响应式设计 */
@media screen and (max-width: 992px) {
#main {
flex-direction: column;
}
#primary {
padding-right: 0;
margin-bottom: 2rem;
}
.widget-area {
padding-left: 0;
}
}
@media screen and (max-width: 768px) {
.site {
margin: 0;
border-radius: 0;
box-shadow: none;
}
.main-navigation ul {
flex-wrap: wrap;
}
.main-navigation li {
margin: 0.25rem;
}
#primary, #secondary {
padding: 0 1rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

@ -2,6 +2,10 @@ from django.urls import path
from django.views.decorators.cache import cache_page
from . import views
from . import article_views
from .article_views_new import ArticleCreateView, ArticleUpdateView
from .article_delete_view import ArticleDeleteView
from . import image_views
app_name = "blog"
urlpatterns = [
@ -17,6 +21,22 @@ urlpatterns = [
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'article/create/',
ArticleCreateView.as_view(),
name='article_create'),
path(
r'article/edit/<int:article_id>/',
ArticleUpdateView.as_view(),
name='article_edit'),
path(
r'article/delete/<int:article_id>/',
ArticleDeleteView.as_view(),
name='article_delete'),
path(
r'my-articles/',
views.my_articles,
name='my_articles'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
@ -59,4 +79,17 @@ urlpatterns = [
r'clean',
views.clean_cache_view,
name='clean'),
# 图片相关路由
path(
'article/<int:article_id>/upload-image/',
image_views.ImageUploadView.as_view(),
name='upload_image'),
path(
'markdown-image-upload/',
image_views.markdown_image_upload,
name='markdown_image_upload'),
path(
'image/<int:image_id>/delete/',
image_views.ImageDeleteView.as_view(),
name='delete_image'),
]

@ -150,6 +150,11 @@ class ArticleDetailView(DetailView):
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
# 获取文章关联的图片
from .models import ArticleImage
article_images = ArticleImage.objects.filter(article=self.object)
kwargs['article_images'] = article_images
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
@ -378,3 +383,102 @@ def permission_denied_view(
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from .forms import ArticleForm
class ArticleCreateView(LoginRequiredMixin, CreateView):
"""创建文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_create.html'
def form_valid(self, form):
form.instance.author = self.request.user
# 处理分类
category_name = form.cleaned_data['category']
category, created = Category.objects.get_or_create(
name=category_name,
defaults={'parent_category': None}
)
form.instance.category = category
# 处理标签
tags_str = form.cleaned_data['tags']
if tags_str:
tag_names = [tag.strip() for tag in tags_str.split(',') if tag.strip()]
form.instance.save() # 保存文章实例,以便可以添加多对多关系
for tag_name in tag_names:
tag, created = Tag.objects.get_or_create(name=tag_name)
form.instance.tags.add(tag)
# 先保存文章,以便可以关联图片
response = super().form_valid(form)
# 将临时图片关联到新创建的文章
from .models import ArticleImage
# 获取会话中的临时图片ID列表
temp_image_ids = self.request.session.get('temp_image_ids', [])
# 查找这些临时图片
temp_images = ArticleImage.objects.filter(id__in=temp_image_ids)
# 将这些图片关联到新创建的文章
for image in temp_images:
image.article = form.instance
image.save()
# 清除会话中的临时图片ID列表
self.request.session['temp_image_ids'] = []
self.request.session.modified = True
return response
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
"""更新文章视图"""
model = Article
form_class = ArticleForm
template_name = 'blog/article_edit.html'
pk_url_kwarg = 'article_id'
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
# 只有文章作者或管理员可以编辑
if obj.author != request.user and not request.user.is_superuser:
return redirect('blog:detailbyid',
article_id=obj.id,
year=obj.creation_time.year,
month=obj.creation_time.month,
day=obj.creation_time.day)
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy('blog:detailbyid', kwargs={
'article_id': self.object.id,
'year': self.object.creation_time.year,
'month': self.object.creation_time.month,
'day': self.object.creation_time.day
})
@login_required
def my_articles(request):
"""显示当前用户的文章列表"""
articles = Article.objects.filter(author=request.user)
return render(request, 'blog/my_articles.html', {'articles': articles})

@ -355,8 +355,53 @@ STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesSto
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'images')
MEDIA_URL = '/images/'
# MDEDITOR配置
MDEDITOR_CONFIGS = {
'default': {
'width': '100%', # 编辑器宽度
'height': 500, # 编辑器高度
'toolbar': ["undo", "redo", "|",
"bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|",
"h1", "h2", "h3", "h5", "h6", "|",
"list-ul", "list-ol", "hr", "|",
"link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|",
"goto-line", "watch", "preview", "fullscreen", "clear", "search", "|",
"help", "info"
],
'upload_image_formats': ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
'image_folder': 'images',
'theme': 'default', # dark / default
'preview_theme': 'default', # dark / default
'editor_theme': 'default', # pastel-on-dark / dark
'toolbar_autofixed': True,
'search_place': 'top', # top / bottom
'language': 'zh-cn', # 语言
'line_numbers': True, # 显示行号
'tab_size': 4, # Tab缩进
'tex': True, # 科学公式TeX支持
'flow_chart': True, # 流程图支持
'sequence': True, # 时序图支持
'mind_map': True, # 脑图支持
'inline_break': True, # 换行符
'line_wrapping': True, # 自动换行
'auto_save': 1000, # 自动保存时间间隔(ms)
'save_html_to textarea': True, # 保存HTML到文本框
'html_decode': True, # HTML解码
'emoji': True, # 表情
'task_list': True, # 任务列表
'katex': True, # 科学公式KaTeX支持
'toc': True, # 目录
'at_link': True, # @link
'email_link': True, # email链接
'image_upload': True, # 图片上传
'image_upload_url': '/mdeditor/upload/', # 图片上传URL
'image_path': 'images/' # 图片上传路径
}
}
X_FRAME_OPTIONS = 'SAMEORIGIN'
# 安全头部配置 - 防XSS和其他攻击

@ -73,6 +73,8 @@ urlpatterns += i18n_patterns(
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@ -665,3 +665,95 @@ msgstr "快捷登录"
#: .\templates\share_layout\nav.html:26
msgid "Article archive"
msgstr "文章归档"
#: .\templates\share_layout\nav.html:29
msgid "Create Article"
msgstr "创建文章"
#: .\templates\share_layout\nav.html:33
msgid "My Articles"
msgstr "我的文章"
#: .\templates\blog\article_create.html:5
msgid "Create Article"
msgstr "创建文章"
#: .\templates\blog\article_edit.html:5
msgid "Edit Article"
msgstr "编辑文章"
#: .\templates\blog\my_articles.html:5
msgid "My Articles"
msgstr "我的文章"
#: .\templates\share_layout\nav.html:29
msgid "发布文章"
msgstr "发布文章"
#: .\templates\share_layout\nav.html:33
msgid "我的文章"
msgstr "我的文章"
#: .\templates\blog\article_create.html:97
msgid "发布"
msgstr "发布"
#: .\templates\blog\article_create.html:98
msgid "取消"
msgstr "取消"
#: .\templates\blog\article_edit.html:97
msgid "更新"
msgstr "更新"
#: .\templates\blog\article_edit.html:98
msgid "取消"
msgstr "取消"
#: .\templates\blog\my_articles.html:13
msgid "发布新文章"
msgstr "发布新文章"
#: .\templates\blog\my_articles.html:22
msgid "标题"
msgstr "标题"
#: .\templates\blog\my_articles.html:23
msgid "状态"
msgstr "状态"
#: .\templates\blog\my_articles.html:24
msgid "分类"
msgstr "分类"
#: .\templates\blog\my_articles.html:25
msgid "创建时间"
msgstr "创建时间"
#: .\templates\blog\my_articles.html:26
msgid "浏览量"
msgstr "浏览量"
#: .\templates\blog\my_articles.html:27
msgid "操作"
msgstr "操作"
#: .\templates\blog\my_articles.html:38
msgid "已发布"
msgstr "已发布"
#: .\templates\blog\my_articles.html:40
msgid "草稿"
msgstr "草稿"
#: .\templates\blog\my_articles.html:48
msgid "编辑"
msgstr "编辑"
#: .\templates\blog\my_articles.html:58
msgid "您还没有创建任何文章。"
msgstr "您还没有创建任何文章。"
#: .\templates\blog\my_articles.html:59
msgid "创建您的第一篇文章"
msgstr "创建您的第一篇文章"

File diff suppressed because it is too large Load Diff

@ -0,0 +1,2 @@
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jU77yQAAAABJRU5ErkJggg==

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save