Update code

develop
flj666 3 months ago
parent 6884ccbc47
commit a9395660f5

@ -0,0 +1 @@
mysql

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -51,10 +51,10 @@ class ArticlelAdmin(admin.ModelAdmin):
'creation_time',
'views',
'status',
'type',
'article_type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
list_filter = ('status', 'article_type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True

@ -27,7 +27,7 @@ def seo_processor(requests):
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
article_type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
type=article.article_type,
views=article.views,
article_order=article.article_order) for article in articles]

@ -0,0 +1,90 @@
# Generated by Django 5.2.4 on 2025-11-23 18:24
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='article',
name='type',
),
migrations.RemoveField(
model_name='links',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='article_type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AddField(
model_name='links',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='blogsettings',
name='beian_code',
field=models.CharField(blank=True, default='', max_length=2000, verbose_name='备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', verbose_name='公共尾部'),
),
migrations.AlterField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', verbose_name='公共头部'),
),
migrations.AlterField(
model_name='blogsettings',
name='gongan_beiancode',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='公安备案号'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, verbose_name='adsense code'),
),
migrations.CreateModel(
name='Favorite',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'favorite',
'verbose_name_plural': 'favorite',
'unique_together': {('user', 'article')},
},
),
migrations.CreateModel(
name='Like',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('creation_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time')),
('last_modify_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'like',
'verbose_name_plural': 'like',
'unique_together': {('user', 'article')},
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-11-23 19:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_remove_article_type_remove_links_last_mod_time_and_more'),
]
operations = [
migrations.AddField(
model_name='article',
name='slug',
field=models.SlugField(blank=True, max_length=200, null=True, verbose_name='slug'),
),
]

@ -3,10 +3,12 @@
import logging
import re
from abc import abstractmethod
from django.utils.functional import cached_property
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Count
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@ -93,6 +95,7 @@ class Article(BaseModel):
('p', _('Page')), # 静态页面
)
title = models.CharField(_('title'), max_length=200, unique=True) #cll 文章标题
slug = models.SlugField(_('slug'), max_length=200, blank=True, null=True) #cll URL友好的标识符
body = MDTextField(_('body')) #cll 文章正文支持Markdown格式
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, default=now) #cll 发布时间
@ -101,7 +104,7 @@ class Article(BaseModel):
max_length=1,
choices=STATUS_CHOICES,
default='p') #cll 文章状态
comment_status_field = models.CharField(
comment_status = models.CharField(
_('comment status'),
max_length=1,
choices=COMMENT_STATUS,
@ -156,9 +159,18 @@ class Article(BaseModel):
names.append(category.name)
return names
#cll 保存文章,更新修改时间
#cll 保存文章,更新修改时间并自动生成slug
def save(self, *args, **kwargs):
self.last_modify_time = now()
# 自动生成slug
if not self.slug:
self.slug = slugify(self.title)
# 确保slug唯一性
original_slug = self.slug
counter = 1
while Article.objects.filter(slug=self.slug).exclude(id=self.id).exists():
self.slug = f"{original_slug}-{counter}"
counter += 1
return super().save(*args, **kwargs)
#cll 增加文章浏览次数
@ -194,6 +206,14 @@ class Article(BaseModel):
return result.group(1)
else:
return None
@cached_property
def likes_count(self):
return self.like_set.count()
@cached_property
def favorites_count(self):
return self.favorite_set.count()
#xy 分类模型
@ -271,6 +291,9 @@ class Tag(BaseModel):
verbose_name_plural = verbose_name #zhj 复数形式
# 文章点赞和收藏模型将在文件末尾使用继承BaseModel的版本定义
#flj 友情链接模型
class Links(models.Model):
name = models.CharField(_('link name'), max_length=30, unique=True) #flj 链接名称
@ -316,6 +339,45 @@ class SideBar(models.Model):
#fkc 网站设置模型
# 点赞模型
class Like(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章点赞一次
verbose_name = _('like')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} likes {self.article.title}"
# 收藏模型
class Favorite(BaseModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('user'),
on_delete=models.CASCADE) # 关联用户
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE) # 关联文章
class Meta:
unique_together = ('user', 'article') # 确保一个用户只能对一篇文章收藏一次
verbose_name = _('favorite')
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} favorites {self.article.title}"
class BlogSettings(models.Model):
site_name = models.CharField(
_('site name'),

@ -0,0 +1,64 @@
{% extends "blog/base.html" %}
{% load staticfiles %}
{% load custom_markdown %}
{% load likes_tags %}
{% load static %}
{% block title %}{{ title }} - {{ site_name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="well well-lg">
<h2 class="text-center">我的收藏</h2>
{% if article_list %}
<div class="article-list">
{% for article in article_list %}
<div class="article">
<div class="title">
<h3><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h3>
</div>
<div class="meta">
<span class="author"><a href="{% url 'blog:author' article.author.username %}">{{ article.author.username }}</a></span>
<span class="views">{{ article.views }} 阅读</span>
<span class="likes">{{ article.total_likes }} 点赞</span>
<span class="favorites">{{ article.total_favorites }} 收藏</span>
<span class="time">{{ article.created_time }}</span>
</div>
<div class="entry-content">
<p>{{ article.body | custom_markdown | truncatechars:200 }}</p>
</div>
<div class="article-footer">
<a href="{{ article.get_absolute_url }}" class="btn btn-primary btn-sm">阅读全文</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info text-center">
<p>您还没有收藏任何文章</p>
</div>
{% endif %}
{% if article_list %}
<div class="pagination">
{% if article_list.has_previous %}
<a href="?page={{ article_list.previous_page_number }}" class="page-link">上一页</a>
{% endif %}
<span class="current-page">{{ article_list.number }} / {{ article_list.paginator.num_pages }}</span>
{% if article_list.has_next %}
<a href="?page={{ article_list.next_page_number }}" class="page-link">下一页</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
{% include "blog/right_sidebar.html" %}
</div>
</div>
</div>
{% endblock %}

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)

@ -73,4 +73,18 @@ urlpatterns = [
r'clean',
views.clean_cache_view,
name='clean'), # 清除缓存接口
# 点赞和收藏功能
path(
r'article/like/',
views.like_article,
name='like_article'), # 文章点赞接口
path(
r'article/favorite/',
views.favorite_article,
name='favorite_article'), # 文章收藏接口
# 我的收藏页面
path(
r'user/favorites/',
views.user_favorites,
name='user_favorites'), # 我的收藏页面
]

@ -20,7 +20,7 @@ from django.views.generic.detail import DetailView # 详情页视图基类
from django.views.generic.list import ListView # 列表页视图基类
from haystack.views import SearchView # 搜索视图
from blog.models import Article, Category, LinkShowType, Links, Tag
from blog.models import Article, Category, LinkShowType, Links, Tag, Like, Favorite
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks # 插件管理
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
@ -116,11 +116,38 @@ class ArticleDetailView(DetailView):
#flj 获取文章详情页的上下文数据,包括评论表单、相关文章等
def get_context_data(self, **kwargs):
#flj 调用父类方法获取基础上下文数据
kwargs = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
article.viewed() # 增加浏览次数
#flj 添加评论表单
#flj 获取文章评论列表
#flj 添加相关文章
comments = article.comment_list()
comment_form = CommentForm()
#flj 添加点赞和收藏信息
is_liked = False
is_favorited = False
like_count = article.like_set.count()
favorite_count = article.favorite_set.count()
if self.request.user.is_authenticated:
is_liked = Like.objects.filter(user=self.request.user, article=article).exists()
is_favorited = Favorite.objects.filter(user=self.request.user, article=article).exists()
#flj 添加到上下文
kwargs['comments'] = comments
kwargs['comment_form'] = comment_form
kwargs['like_count'] = like_count
kwargs['is_liked'] = is_liked
kwargs['favorite_count'] = favorite_count
kwargs['is_favorited'] = is_favorited
#flj 调用插件处理文章内容
return super().get_context_data(**kwargs)
article_content = article.body_to_string()
article_content = hooks.call_hook(ARTICLE_CONTENT_HOOK_NAME, request=self.request, content=article_content)
article.body = article_content
return kwargs
#flj 分类详情视图,显示指定分类下的文章列表
@ -310,3 +337,113 @@ def clean_cache_view(request):
#flj 清理缓存
#flj 返回成功信息
return HttpResponse(_('清理缓存成功'))
# 点赞视图
@csrf_exempt
def like_article(request):
'''
处理文章点赞
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
like, created = Like.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经点赞,则取消点赞
like.delete()
# 获取最新的点赞数
count = article.like_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能点赞')
}), content_type="application/json")
# 收藏视图
@csrf_exempt
def favorite_article(request):
'''
处理文章收藏
'''
if request.method == 'POST' and request.user.is_authenticated:
try:
data = json.loads(request.body)
article_id = data.get('article_id')
article = Article.objects.get(id=article_id)
favorite, created = Favorite.objects.get_or_create(user=request.user, article=article)
if not created:
# 如果已经收藏,则取消收藏
favorite.delete()
# 获取最新的收藏数
count = article.favorite_set.count()
return HttpResponse(json.dumps({
'status': 'success',
'count': count
}), content_type="application/json")
except Article.DoesNotExist:
return HttpResponse(json.dumps({
'status': 'error',
'message': _('文章不存在')
}), content_type="application/json")
except Exception as e:
return HttpResponse(json.dumps({
'status': 'error',
'message': str(e)
}), content_type="application/json")
return HttpResponse(json.dumps({
'status': 'error',
'message': _('需要登录才能收藏')
}), content_type="application/json")
# 我的收藏页面视图
def user_favorites(request):
'''
显示用户收藏的文章列表
'''
if not request.user.is_authenticated:
return render(request, ERROR_PAGE_TEMPLATE, {'message': _('请先登录')}, status=403)
# 获取用户收藏的文章
favorites = Favorite.objects.filter(user=request.user).order_by('-created_at')
# 提取文章并进行分页
articles = [fav.article for fav in favorites if fav.article.status == 'p']
paginator = Paginator(articles, settings.PAGINATE_BY)
page = request.GET.get('page', 1)
try:
article_list = paginator.page(page)
except Exception:
article_list = paginator.page(1)
context = {
'article_list': article_list,
'title': _('我的收藏'),
'page_type': '我的收藏',
'has_favorites': len(articles) > 0
}
return render(request, 'blog/user_favorites.html', context)

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.filter('term', article_type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title

@ -159,7 +159,7 @@ USE_TZ = False # 不使用UTC时间
# 静态文件配置
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录
STATIC_URL = '/static/' # 静态文件URL前缀
STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件源目录
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # 静态文件源目录
# 搜索配置
HAYSTACK_CONNECTIONS = {
@ -303,8 +303,11 @@ STATICFILES_FINDERS = (
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 在开发环境中动态启用压缩根据DEBUG状态自动调整
COMPRESS_ENABLED = not DEBUG # 调试模式下禁用压缩
COMPRESS_OFFLINE = False # 禁用离线模式
COMPRESS_ROOT = os.path.join(BASE_DIR, 'static') # 直接指向静态文件源目录
COMPRESS_URL = STATIC_URL # 确保COMPRESS_URL与STATIC_URL一致
COMPRESS_CSS_FILTERS = [

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc"
article.author = user
article.category = c
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
s = TextMessage([])

@ -48,7 +48,7 @@ class AccountTest(TestCase):
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -96,7 +96,7 @@ class AccountTest(TestCase):
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -197,7 +197,7 @@ class ArticleDocumentManager():
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
type=article.article_type,
views=article.views,
article_order=article.article_order) for article in articles]

@ -86,7 +86,7 @@ class Article(BaseModel):
max_length=1,
choices=COMMENT_STATUS,
default='o')
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
article_type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a')
views = models.PositiveIntegerField(_('views'), default=0)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,

@ -59,7 +59,7 @@ class ArticleTest(TestCase):
article.body = "nicecontent"
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
@ -74,7 +74,7 @@ class ArticleTest(TestCase):
article.body = "nicetitle" + str(i)
article.author = user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)

@ -97,7 +97,7 @@ class IndexView(ArticleListView):
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
article_list = Article.objects.filter(article_type='a', status='p')
return article_list
def get_queryset_cache_key(self):
@ -216,7 +216,7 @@ class AuthorDetailView(ArticleListView):
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
author__username=author_name, article_type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
@ -238,7 +238,7 @@ class TagDetailView(ArticleListView):
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
tags__name=tag_name, article_type='a', status='p')
return article_list
def get_queryset_cache_key(self):

@ -42,7 +42,7 @@ class CommentsTest(TransactionTestCase):
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()

@ -91,7 +91,7 @@ class ElasticSearchBackend(BaseSearchBackend):
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.filter('term', article_type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()

@ -21,7 +21,7 @@ class DjangoBlogFeed(Feed):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
return Article.objects.filter(article_type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title

@ -37,7 +37,7 @@ class ServerManagerTest(TestCase):
article.body = "nicecontentccc"
article.author = user
article.category = c
article.type = 'a'
article.article_type = 'a'
article.status = 'p'
article.save()
s = TextMessage([])

@ -8,7 +8,7 @@
<main id="content">
{% load_article_detail article False user %}
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}

@ -37,7 +37,7 @@
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}

@ -8,7 +8,7 @@
</a>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if article.tags.all %}
{% trans 'and tagged' %}

@ -8,7 +8,7 @@
<main id="content">
{% load_article_detail article False user %}
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
<nav class="nav-single">
<h3 class="assistive-text">文章导航</h3>
{% if next_article %}

@ -37,7 +37,7 @@
</div>
</div><!-- .comments-link -->
<br/>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if not isindex %}
{% cache 36000 breadcrumb article.pk %}
{% load_breadcrumb article %}
@ -69,6 +69,82 @@
</div><!-- .entry-content -->
{% if not isindex %}
<div class="article-engagement" style="margin: 1rem 0; padding: 1rem; border-top: 1px solid var(--border-color);">
<button class="engagement-btn like-btn" data-article-id="{{ article.id }}">
<span class="icon">👍</span>
<span class="count">{{ article.likes_count|default:0 }}</span>
<span class="text">{% trans 'Like' %}</span>
</button>
<button class="engagement-btn favorite-btn" data-article-id="{{ article.id }}">
<span class="icon"></span>
<span class="count">{{ article.favorites_count|default:0 }}</span>
<span class="text">{% trans 'Favorite' %}</span>
</button>
{% if not user.is_authenticated %}
<div class="auth-hint">
<span><a href="{% url 'account:login' %}?next={{ request.path }}">登录</a> 后进行操作</span>
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 点赞功能
document.querySelector('.like-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/like/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
// 收藏功能
document.querySelector('.favorite-btn').addEventListener('click', function() {
const articleId = this.dataset.articleId;
const countEl = this.querySelector('.count');
fetch(`/article/favorite/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({ article_id: articleId })
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
this.classList.toggle('is-active');
countEl.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => console.error('Error:', error));
});
});
</script>
{% endif %}
{% load_article_metas article user %}
</article><!-- #post -->

@ -8,7 +8,7 @@
</a>
{% if article.type == 'a' %}
{% if article.article_type == 'a' %}
{% if article.tags.all %}
{% trans 'and tagged' %}

@ -0,0 +1,139 @@
{% extends 'blog/base.html' %}
{% load i18n %}
{% load comment_tags %}
{% load staticfiles %}
{% load custom_markdown %}
{% block title %}{% trans "我的收藏" %} - {{ SITE_NAME }}{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
<h2>{% trans "我的收藏" %}</h2>
</div>
</div>
<div class="panel-body">
{% if has_favorites %}
{% for article in article_list %}
<div class="article post-preview">
<div class="article-inner">
<h3 class="article-title">
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</h3>
<div class="article-meta">
<span class="article-date">
<i class="fa fa-calendar"></i>
{{ article.created_time }}
</span>
<span class="article-category">
<i class="fa fa-folder-o"></i>
<a href="{{ article.category.get_absolute_url }}">{{ article.category.name }}</a>
</span>
<span class="article-author">
<i class="fa fa-user-o"></i>
<a href="{{ article.author.get_absolute_url }}">{{ article.author.nickname }}</a>
</span>
<span class="article-view-count">
<i class="fa fa-eye"></i>
{{ article.views }}
</span>
<span class="article-comment-count">
<i class="fa fa-comment-o"></i>
{% get_comment_count article %}
</span>
</div>
<div class="article-content">
<p>
{{ article.body|custom_markdown|truncatechars:100 }}
</p>
</div>
<div class="article-footer">
<div class="article-more-link">
<a href="{{ article.get_absolute_url }}" class="btn btn-default">
阅读全文
</a>
</div>
</div>
</div>
</div>
<hr>
{% endfor %}
{% if page_obj.has_previous or page_obj.has_next %}
<div class="pagination">
<ul class="pagination">
{% if page_obj.has_previous %}
<li>
<a href="?page={{ page_obj.previous_page_number }}">{% trans "上一页" %}</a>
</li>
{% endif %}
{% if page_obj.has_next %}
<li>
<a href="?page={{ page_obj.next_page_number }}">{% trans "下一页" %}</a>
</li>
{% endif %}
</ul>
</div>
{% endif %}
{% else %}
<div class="no-favorites">
<p class="text-center">您还没有收藏任何文章</p>
<div class="text-center">
<a href="/" class="btn btn-primary">
去首页看看
</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
{% include 'blog/tags.html' %}
{% include 'blog/hotest_articles.html' %}
{% include 'blog/archives.html' %}
</div>
</div>
</div>
{% endblock %}
{% block extra_footer %}
<style>
.article-meta {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.article-meta span {
margin-right: 15px;
}
.article-meta a {
color: #666;
}
.article-meta a:hover {
color: #337ab7;
text-decoration: none;
}
.article-title {
margin-bottom: 10px;
}
.article-title a {
color: #333;
}
.article-title a:hover {
color: #337ab7;
text-decoration: none;
}
.no-favorites {
padding: 30px 0;
}
.no-favorites p {
font-size: 16px;
color: #666;
margin-bottom: 20px;
}
</style>
{% endblock %}

@ -25,6 +25,11 @@
<a href="{% url "blog:archives" %}">{% trans 'Article archive' %}</a>
</li>
{% if user.is_authenticated %}
<li class="menu-item menu-item-type-taxonomy menu-item-object-category">
<a href="{% url "blog:user_favorites" %}">{% trans '我的收藏' %}</a>
</li>
{% endif %}
</ul>
</div>
</nav><!-- #site-navigation -->
Loading…
Cancel
Save