Skip to content

Commit 99a37f9

Browse files
authored
Merge pull request #766 from Inkwhs/master
收藏系统
2 parents 0a30246 + 36670b1 commit 99a37f9

File tree

8 files changed

+298
-6
lines changed

8 files changed

+298
-6
lines changed

blog/migrations/0007_favorite.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Generated by Django 5.2.1 on 2025-05-17 10:30
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
dependencies = [
11+
("blog", "0006_alter_blogsettings_options"),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="Favorite",
18+
fields=[
19+
("id", models.AutoField(primary_key=True, serialize=False)),
20+
(
21+
"creation_time",
22+
models.DateTimeField(
23+
default=django.utils.timezone.now, verbose_name="creation time"
24+
),
25+
),
26+
(
27+
"last_modify_time",
28+
models.DateTimeField(
29+
default=django.utils.timezone.now, verbose_name="modify time"
30+
),
31+
),
32+
(
33+
"article",
34+
models.ForeignKey(
35+
on_delete=django.db.models.deletion.CASCADE,
36+
to="blog.article",
37+
verbose_name="article",
38+
),
39+
),
40+
(
41+
"user",
42+
models.ForeignKey(
43+
on_delete=django.db.models.deletion.CASCADE,
44+
to=settings.AUTH_USER_MODEL,
45+
verbose_name="user",
46+
),
47+
),
48+
],
49+
options={
50+
"verbose_name": "favorite",
51+
"verbose_name_plural": "favorite",
52+
"unique_together": {("user", "article")},
53+
},
54+
),
55+
]

blog/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,26 @@ def save(self, *args, **kwargs):
363363
super().save(*args, **kwargs)
364364
from djangoblog.utils import cache
365365
cache.clear()
366+
367+
368+
class Favorite(BaseModel):
369+
"""文章收藏"""
370+
user = models.ForeignKey(
371+
settings.AUTH_USER_MODEL,
372+
verbose_name=_('user'),
373+
on_delete=models.CASCADE)
374+
article = models.ForeignKey(
375+
'Article',
376+
verbose_name=_('article'),
377+
on_delete=models.CASCADE)
378+
379+
class Meta:
380+
verbose_name = _('favorite')
381+
verbose_name_plural = verbose_name
382+
unique_together = ('user', 'article') # 防止重复收藏
383+
384+
def __str__(self):
385+
return f'{self.user.username} - {self.article.title}'
386+
387+
def get_absolute_url(self):
388+
return self.article.get_absolute_url()

blog/static/blog/js/blog.js

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,93 @@ window.onload = function () {
8888
// selector.on('change', function () {
8989
// form.submit();
9090
// });
91-
// });
91+
// });
92+
93+
// 获取CSRF Token的函数
94+
function getCookie(name) {
95+
let cookieValue = null;
96+
if (document.cookie && document.cookie !== '') {
97+
const cookies = document.cookie.split(';');
98+
for (let i = 0; i < cookies.length; i++) {
99+
const cookie = cookies[i].trim();
100+
if (cookie.substring(0, name.length + 1) === (name + '=')) {
101+
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
102+
break;
103+
}
104+
}
105+
}
106+
return cookieValue;
107+
}
108+
109+
// 文章详情页收藏功能
110+
function initArticleFavorite() {
111+
const favoriteBtn = document.getElementById('favoriteBtn');
112+
if (!favoriteBtn) return;
113+
114+
const favoriteText = document.getElementById('favoriteText');
115+
const articleId = favoriteBtn.dataset.articleId;
116+
117+
// 检查是否已收藏
118+
fetch(`/favorite/check/${articleId}/`)
119+
.then(response => response.json())
120+
.then(data => {
121+
if (data.is_favorite) {
122+
favoriteBtn.classList.remove('btn-primary');
123+
favoriteBtn.classList.add('btn-danger');
124+
favoriteText.textContent = '取消收藏';
125+
}
126+
});
127+
128+
favoriteBtn.addEventListener('click', function() {
129+
const isFavorite = favoriteBtn.classList.contains('btn-danger');
130+
const url = isFavorite ? `/favorite/remove/${articleId}/` : `/favorite/add/${articleId}/`;
131+
132+
fetch(url, {
133+
method: 'POST',
134+
headers: {
135+
'X-CSRFToken': getCookie('csrftoken')
136+
}
137+
})
138+
.then(response => response.json())
139+
.then(data => {
140+
if (data.status === 'success') {
141+
if (isFavorite) {
142+
favoriteBtn.classList.remove('btn-danger');
143+
favoriteBtn.classList.add('btn-primary');
144+
favoriteText.textContent = '收藏文章';
145+
} else {
146+
favoriteBtn.classList.remove('btn-primary');
147+
favoriteBtn.classList.add('btn-danger');
148+
favoriteText.textContent = '取消收藏';
149+
}
150+
}
151+
});
152+
});
153+
}
154+
155+
// 收藏列表页功能
156+
function initFavoriteList() {
157+
document.querySelectorAll('.remove-favorite').forEach(button => {
158+
button.addEventListener('click', function() {
159+
const articleId = this.dataset.articleId;
160+
fetch(`/favorite/remove/${articleId}/`, {
161+
method: 'POST',
162+
headers: {
163+
'X-CSRFToken': getCookie('csrftoken')
164+
}
165+
})
166+
.then(response => response.json())
167+
.then(data => {
168+
if (data.status === 'success') {
169+
this.closest('.article-item').remove();
170+
}
171+
});
172+
});
173+
});
174+
}
175+
176+
// 初始化所有功能
177+
document.addEventListener('DOMContentLoaded', function() {
178+
initArticleFavorite();
179+
initFavoriteList();
180+
});

blog/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@
5959
r'clean',
6060
views.clean_cache_view,
6161
name='clean'),
62+
path('favorite/add/<int:article_id>/', views.add_favorite, name='add_favorite'),
63+
path('favorite/remove/<int:article_id>/', views.remove_favorite, name='remove_favorite'),
64+
path('favorite/check/<int:article_id>/', views.check_favorite, name='check_favorite'),
65+
path('favorites/', views.favorite_list, name='favorite_list'),
6266
]

blog/views.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
from django.conf import settings
66
from django.core.paginator import Paginator
7-
from django.http import HttpResponse, HttpResponseForbidden
8-
from django.shortcuts import get_object_or_404
9-
from django.shortcuts import render
7+
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
8+
from django.shortcuts import get_object_or_404, render
109
from django.templatetags.static import static
1110
from django.utils import timezone
1211
from django.utils.translation import gettext_lazy as _
1312
from django.views.decorators.csrf import csrf_exempt
1413
from django.views.generic.detail import DetailView
1514
from django.views.generic.list import ListView
1615
from haystack.views import SearchView
16+
from django.contrib.auth.decorators import login_required
1717

18-
from blog.models import Article, Category, LinkShowType, Links, Tag
18+
from blog.models import Article, Category, LinkShowType, Links, Tag, Favorite
1919
from comments.forms import CommentForm
2020
from djangoblog.utils import cache, get_blog_setting, get_sha256
2121

@@ -373,3 +373,46 @@ def permission_denied_view(
373373
def clean_cache_view(request):
374374
cache.clear()
375375
return HttpResponse('ok')
376+
377+
378+
@login_required
379+
def add_favorite(request, article_id):
380+
article = get_object_or_404(Article, id=article_id)
381+
favorite, created = Favorite.objects.get_or_create(
382+
user=request.user,
383+
article=article
384+
)
385+
return JsonResponse({
386+
'status': 'success',
387+
'message': '收藏成功' if created else '已经收藏过了'
388+
})
389+
390+
@login_required
391+
def remove_favorite(request, article_id):
392+
article = get_object_or_404(Article, id=article_id)
393+
Favorite.objects.filter(
394+
user=request.user,
395+
article=article
396+
).delete()
397+
return JsonResponse({
398+
'status': 'success',
399+
'message': '取消收藏成功'
400+
})
401+
402+
@login_required
403+
def favorite_list(request):
404+
favorites = Favorite.objects.filter(user=request.user).select_related('article')
405+
return render(request, 'blog/favorite_list.html', {
406+
'favorites': favorites
407+
})
408+
409+
@login_required
410+
def check_favorite(request, article_id):
411+
article = get_object_or_404(Article, id=article_id)
412+
is_favorite = Favorite.objects.filter(
413+
user=request.user,
414+
article=article
415+
).exists()
416+
return JsonResponse({
417+
'is_favorite': is_favorite
418+
})

templates/blog/article_detail.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{% extends 'share_layout/base.html' %}
22
{% load blog_tags %}
3+
{% load static %}
34

45
{% block header %}
56
<title>{{ article.title }} | {{ SITE_DESCRIPTION }}</title>
@@ -32,6 +33,15 @@
3233
<div id="content" role="main">
3334
{% load_article_detail article False user %}
3435

36+
{% if user.is_authenticated %}
37+
<div class="favorite-container" style="margin: 20px 0;">
38+
<button id="favoriteBtn" class="btn btn-primary" data-article-id="{{ article.id }}">
39+
<i class="fa fa-star"></i>
40+
<span id="favoriteText">收藏文章</span>
41+
</button>
42+
</div>
43+
{% endif %}
44+
3545
{% if article.type == 'a' %}
3646
<nav class="nav-single">
3747
<h3 class="assistive-text">文章导航</h3>
@@ -69,8 +79,13 @@ <h3 class="comment-meta">您还没有登录,请您<a
6979
{% endif %}
7080
</div><!-- #primary -->
7181

82+
{% if user.is_authenticated %}
83+
{% block extra_js %}
84+
<script src="{% static 'blog/js/blog.js' %}"></script>
85+
{% endblock %}
86+
{% endif %}
7287
{% endblock %}
7388

7489
{% block sidebar %}
7590
{% load_sidebar user "p" %}
76-
{% endblock %}
91+
{% endblock %}

templates/blog/favorite_list.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{% extends 'share_layout/base.html' %}
2+
{% load blog_tags %}
3+
{% load i18n %}
4+
{% load static %}
5+
6+
{% block title %}
7+
我的收藏 - {{ SITE_NAME }}
8+
{% endblock %}
9+
10+
{% block content %}
11+
<div class="container">
12+
<div class="row">
13+
<div class="col-md-8">
14+
<div class="card">
15+
<div class="card-header">
16+
<h3 class="card-title">我的收藏</h3>
17+
</div>
18+
<div class="card-body">
19+
{% if favorites %}
20+
<div class="article-list">
21+
{% for favorite in favorites %}
22+
<div class="article-item">
23+
<h2 class="article-title">
24+
<a href="{{ favorite.article.get_absolute_url }}">
25+
{{ favorite.article.title }}
26+
</a>
27+
</h2>
28+
<div class="article-meta">
29+
<span class="article-date">
30+
收藏于: {{ favorite.creation_time|date:"Y-m-d H:i" }}
31+
</span>
32+
<span class="article-category">
33+
分类: <a href="{{ favorite.article.category.get_absolute_url }}">
34+
{{ favorite.article.category.name }}
35+
</a>
36+
</span>
37+
<button class="btn btn-sm btn-danger remove-favorite"
38+
data-article-id="{{ favorite.article.id }}">
39+
取消收藏
40+
</button>
41+
</div>
42+
</div>
43+
{% endfor %}
44+
</div>
45+
{% else %}
46+
<p class="text-center">还没有收藏任何文章</p>
47+
{% endif %}
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
{% endblock %}
54+
55+
{% block extra_js %}
56+
<script src="{% static 'blog/js/blog.js' %}"></script>
57+
{% endblock %}

templates/share_layout/nav.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home menu-item-3498">
88
<a href="/">{% trans 'index' %}</a></li>
99

10+
<li class="menu-item">
11+
<a href="{% url 'blog:favorite_list' %}">
12+
<i class="fa fa-star"></i> 我的收藏
13+
</a>
14+
</li>
15+
1016
{% load blog_tags %}
1117
{% query nav_category_list parent_category=None as root_categorys %}
1218
{% for node in root_categorys %}

0 commit comments

Comments
 (0)