Add Search button

This commit is contained in:
2025-08-11 23:42:14 +08:00
parent b6bbb90703
commit 958b087f54
8 changed files with 330 additions and 187 deletions

View File

@@ -9,32 +9,38 @@ import csv
from django.http import HttpResponse from django.http import HttpResponse
import json import json
# 创建自定义管理站点 # 创建自定义管理站点
class NewsCnAdminSite(AdminSite): class NewsCnAdminSite(AdminSite):
site_header = "新华网管理后台" site_header = "新华网管理后台"
site_title = "新华网管理" site_title = "新华网管理"
index_title = "新华网内容管理" index_title = "新华网内容管理"
class DongfangyancaoAdminSite(AdminSite): class DongfangyancaoAdminSite(AdminSite):
site_header = "东方烟草报管理后台" site_header = "东方烟草报管理后台"
site_title = "东方烟草报管理" site_title = "东方烟草报管理"
index_title = "东方烟草报内容管理" index_title = "东方烟草报内容管理"
# 实例化管理站点 # 实例化管理站点
news_cn_admin = NewsCnAdminSite(name='news_cn_admin') news_cn_admin = NewsCnAdminSite(name='news_cn_admin')
dongfangyancao_admin = DongfangyancaoAdminSite(name='dongfangyancao_admin') dongfangyancao_admin = DongfangyancaoAdminSite(name='dongfangyancao_admin')
@admin.register(Website) @admin.register(Website)
class WebsiteAdmin(admin.ModelAdmin): class WebsiteAdmin(admin.ModelAdmin):
list_display = ('name', 'base_url', 'enabled') list_display = ('name', 'base_url', 'enabled')
# 为ArticleAdmin添加自定义动作 # 为ArticleAdmin添加自定义动作
@admin.register(Article) @admin.register(Article)
class ArticleAdmin(admin.ModelAdmin): class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'website', 'pub_date') list_display = ('title', 'website', 'pub_date')
search_fields = ('title', 'content') search_fields = ('title', 'content')
# 添加动作选项 # 添加动作选项
actions = ['delete_selected_articles', 'delete_dongfangyancao_articles', 'export_as_csv', 'export_as_json', 'export_as_word'] actions = ['delete_selected_articles', 'delete_dongfangyancao_articles', 'export_as_csv', 'export_as_json',
'export_as_word']
def delete_dongfangyancao_articles(self, request, queryset): def delete_dongfangyancao_articles(self, request, queryset):
"""一键删除东方烟草报的所有文章""" """一键删除东方烟草报的所有文章"""
@@ -61,7 +67,8 @@ class ArticleAdmin(admin.ModelAdmin):
writer.writerow(field_names) writer.writerow(field_names)
for obj in queryset: for obj in queryset:
row = [getattr(obj, field)() if callable(getattr(obj, field)) else getattr(obj, field) for field in field_names] row = [getattr(obj, field)() if callable(getattr(obj, field)) else getattr(obj, field) for field in
field_names]
writer.writerow(row) writer.writerow(row)
return response return response
@@ -114,7 +121,8 @@ class ArticleAdmin(admin.ModelAdmin):
# 添加文章元数据 # 添加文章元数据
doc.add_paragraph(f"网站: {article.website.name}") doc.add_paragraph(f"网站: {article.website.name}")
doc.add_paragraph(f"URL: {article.url}") doc.add_paragraph(f"URL: {article.url}")
doc.add_paragraph(f"发布时间: {article.pub_date.strftime('%Y-%m-%d %H:%M:%S') if article.pub_date else 'N/A'}") doc.add_paragraph(
f"发布时间: {article.pub_date.strftime('%Y-%m-%d %H:%M:%S') if article.pub_date else 'N/A'}")
doc.add_paragraph(f"创建时间: {article.created_at.strftime('%Y-%m-%d %H:%M:%S')}") doc.add_paragraph(f"创建时间: {article.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
# 添加文章内容 # 添加文章内容
@@ -190,12 +198,14 @@ class ArticleAdmin(admin.ModelAdmin):
# 创建HttpResponse # 创建HttpResponse
from django.http import HttpResponse from django.http import HttpResponse
response = HttpResponse(buffer.getvalue(), content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document') response = HttpResponse(buffer.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document')
response['Content-Disposition'] = 'attachment; filename=articles.docx' response['Content-Disposition'] = 'attachment; filename=articles.docx'
return response return response
export_as_word.short_description = "导出选中文章为Word格式" export_as_word.short_description = "导出选中文章为Word格式"
# 为不同网站创建专门的文章管理类 # 为不同网站创建专门的文章管理类
class NewsCnArticleAdmin(admin.ModelAdmin): class NewsCnArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'pub_date') list_display = ('title', 'pub_date')
@@ -258,6 +268,7 @@ class NewsCnArticleAdmin(admin.ModelAdmin):
export_as_json.short_description = "导出选中文章为JSON格式" export_as_json.short_description = "导出选中文章为JSON格式"
class DongfangyancaoArticleAdmin(admin.ModelAdmin): class DongfangyancaoArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'pub_date') list_display = ('title', 'pub_date')
search_fields = ('title', 'content') search_fields = ('title', 'content')
@@ -329,6 +340,7 @@ class DongfangyancaoArticleAdmin(admin.ModelAdmin):
export_as_json.short_description = "导出选中文章为JSON格式" export_as_json.short_description = "导出选中文章为JSON格式"
# 在各自的管理站点中注册模型 # 在各自的管理站点中注册模型
news_cn_admin.register(Website, WebsiteAdmin) news_cn_admin.register(Website, WebsiteAdmin)
news_cn_admin.register(Article, NewsCnArticleAdmin) news_cn_admin.register(Article, NewsCnArticleAdmin)

View File

@@ -1,4 +1,3 @@
# core/management/commands/crawl_dongfangyancao.py
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from core.models import Website from core.models import Website
from core.utils import full_site_crawler from core.utils import full_site_crawler

View File

@@ -1,4 +1,3 @@
# core/management/commands/crawl_xinhua.py
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from core.models import Website from core.models import Website
from core.utils import full_site_crawler from core.utils import full_site_crawler

View File

@@ -103,7 +103,8 @@ class Command(BaseCommand):
writer.writeheader() writer.writeheader()
for article_data in articles_data: for article_data in articles_data:
# 将列表转换为字符串以便在CSV中存储 # 将列表转换为字符串以便在CSV中存储
article_data['media_files'] = ';'.join(article_data['media_files']) if article_data['media_files'] else '' article_data['media_files'] = ';'.join(article_data['media_files']) if article_data[
'media_files'] else ''
writer.writerow(article_data) writer.writerow(article_data)
# 添加Word格式导出方法 # 添加Word格式导出方法
@@ -215,7 +216,8 @@ class Command(BaseCommand):
writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames) writer = csv.DictWriter(csv_buffer, fieldnames=fieldnames)
writer.writeheader() writer.writeheader()
for article_data in articles_data: for article_data in articles_data:
article_data['media_files'] = ';'.join(article_data['media_files']) if article_data['media_files'] else '' article_data['media_files'] = ';'.join(article_data['media_files']) if article_data[
'media_files'] else ''
writer.writerow(article_data) writer.writerow(article_data)
zipf.writestr(data_filename, csv_buffer.getvalue()) zipf.writestr(data_filename, csv_buffer.getvalue())
# 添加Word格式支持 # 添加Word格式支持

View File

@@ -13,39 +13,46 @@
padding: 20px; padding: 20px;
background-color: #f8f9fa; background-color: #f8f9fa;
} }
.article-container { .article-container {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px; padding: 30px;
margin-bottom: 20px; margin-bottom: 20px;
} }
h1 { h1 {
color: #2c3e50; color: #2c3e50;
border-bottom: 2px solid #3498db; border-bottom: 2px solid #3498db;
padding-bottom: 10px; padding-bottom: 10px;
margin-top: 0; margin-top: 0;
} }
.meta { .meta {
color: #7f8c8d; color: #7f8c8d;
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 20px; margin-bottom: 20px;
} }
hr { hr {
border: 0; border: 0;
height: 1px; height: 1px;
background: #ecf0f1; background: #ecf0f1;
margin: 20px 0; margin: 20px 0;
} }
.content { .content {
font-size: 16px; font-size: 16px;
} }
.content img { .content img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 4px; border-radius: 4px;
margin: 10px 0; margin: 10px 0;
} }
.back-link { .back-link {
display: inline-block; display: inline-block;
padding: 10px 20px; padding: 10px 20px;
@@ -55,13 +62,14 @@
border-radius: 4px; border-radius: 4px;
transition: background-color 0.3s; transition: background-color 0.3s;
} }
.back-link:hover { .back-link:hover {
background-color: #2980b9; background-color: #2980b9;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="article-container"> <div class="article-container">
<h1>{{ article.title }}</h1> <h1>{{ article.title }}</h1>
<div class="meta"> <div class="meta">
<p>发布时间: {{ article.pub_date|date:"Y-m-d H:i" }}</p> <p>发布时间: {{ article.pub_date|date:"Y-m-d H:i" }}</p>
@@ -72,6 +80,6 @@
</div> </div>
<hr/> <hr/>
<p><a href="{% url 'article_list' %}" class="back-link">← 返回列表</a></p> <p><a href="{% url 'article_list' %}" class="back-link">← 返回列表</a></p>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -13,25 +13,29 @@
padding: 20px; padding: 20px;
background-color: #f8f9fa; background-color: #f8f9fa;
} }
.container { .container {
background: white; background: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px; padding: 30px;
margin-bottom: 20px; margin-bottom: 20px;
} }
h1 { h1 {
color: #2c3e50; color: #2c3e50;
border-bottom: 2px solid #3498db; border-bottom: 2px solid #3498db;
padding-bottom: 10px; padding-bottom: 10px;
margin-top: 0; margin-top: 0;
} }
.filters { .filters {
margin-bottom: 20px; margin-bottom: 20px;
padding: 15px; padding: 15px;
background-color: #f1f8ff; background-color: #f1f8ff;
border-radius: 5px; border-radius: 5px;
} }
.filters a { .filters a {
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 5px 10px;
@@ -41,38 +45,47 @@
text-decoration: none; text-decoration: none;
border-radius: 3px; border-radius: 3px;
} }
.filters a.active { .filters a.active {
background-color: #3498db; background-color: #3498db;
color: white; color: white;
} }
ul { ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
li { li {
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid #ecf0f1; border-bottom: 1px solid #ecf0f1;
} }
li:last-child { li:last-child {
border-bottom: none; border-bottom: none;
} }
a { a {
color: #3498db; color: #3498db;
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
color: #2980b9; color: #2980b9;
text-decoration: underline; text-decoration: underline;
} }
.meta { .meta {
color: #7f8c8d; color: #7f8c8d;
font-size: 0.9em; font-size: 0.9em;
} }
.pagination { .pagination {
margin-top: 30px; margin-top: 30px;
text-align: center; text-align: center;
padding: 20px 0; padding: 20px 0;
} }
.pagination a { .pagination a {
display: inline-block; display: inline-block;
padding: 8px 16px; padding: 8px 16px;
@@ -82,38 +95,101 @@
border-radius: 4px; border-radius: 4px;
margin: 0 2px; /* 修改:调整页码间距 */ margin: 0 2px; /* 修改:调整页码间距 */
} }
.pagination a:hover { .pagination a:hover {
background-color: #2980b9; background-color: #2980b9;
} }
.pagination span { .pagination span {
margin: 0 10px; margin: 0 10px;
color: #7f8c8d; color: #7f8c8d;
} }
/* 新增:当前页码样式 */ /* 新增:当前页码样式 */
.pagination .current { .pagination .current {
background-color: #2980b9; background-color: #2980b9;
cursor: default; cursor: default;
} }
/* 新增:省略号样式 */ /* 新增:省略号样式 */
.pagination .ellipsis { .pagination .ellipsis {
display: inline-block; display: inline-block;
padding: 8px 4px; padding: 8px 4px;
color: #7f8c8d; color: #7f8c8d;
} }
/* 新增:搜索框样式 */
.search-form {
margin-bottom: 20px;
padding: 15px;
background-color: #f1f8ff;
border-radius: 5px;
}
.search-form input[type="text"] {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
margin-right: 10px;
}
.search-form input[type="submit"] {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.search-form input[type="submit"]:hover {
background-color: #2980b9;
}
.search-info {
color: #7f8c8d;
font-size: 0.9em;
margin-bottom: 10px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>绿色课堂文章列表</h1> <h1>绿色课堂文章列表</h1>
<!-- 新增:返回首页链接 -->
<div style="margin-bottom: 20px;">
<a href="{% url 'article_list' %}" style="color: #3498db; text-decoration: none;">&larr; 返回首页</a>
</div>
<!-- 新增:搜索表单 -->
<div class="search-form">
<form method="get">
<input type="text" name="q" placeholder="输入关键词搜索文章..." value="{{ search_query }}">
{% if selected_website %}
<input type="hidden" name="website" value="{{ selected_website.id }}">
{% endif %}
<input type="submit" value="搜索">
</form>
</div>
<div class="filters"> <div class="filters">
<strong>按网站筛选:</strong> <strong>按网站筛选:</strong>
<a href="{% url 'article_list' %}" {% if not selected_website %}class="active"{% endif %}>全部</a> <a href="{% url 'article_list' %}{% if search_query %}?q={{ search_query }}{% endif %}" {% if not selected_website %}class="active" {% endif %}>全部</a>
{% for website in websites %} {% for website in websites %}
<a href="?website={{ website.id }}" {% if selected_website.id == website.id %}class="active"{% endif %}>{{ website.name }}</a> <a href="?website={{ website.id }}{% if search_query %}&q={{ search_query }}{% endif %}" {% if selected_website and selected_website.id == website.id %}class="active" {% endif %}>{{ website.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
<!-- 新增:搜索结果信息 -->
{% if search_query %}
<div class="search-info">
搜索 "{{ search_query }}" 找到 {{ page_obj.paginator.count }} 篇文章
<a href="{% if selected_website %}?website={{ selected_website.id }}{% else %}{% url 'article_list' %}{% endif %}">清除搜索</a>
</div>
{% endif %}
<ul> <ul>
{% for article in page_obj %} {% for article in page_obj %}
<li> <li>
@@ -128,11 +204,11 @@
<div class="pagination"> <div class="pagination">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
{% if selected_website %} {% if selected_website %}
<a href="?website={{ selected_website.id }}&page=1">&laquo; 首页</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page=1">&laquo; 首页</a>
<a href="?website={{ selected_website.id }}&page={{ page_obj.previous_page_number }}">上一页</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ page_obj.previous_page_number }}">上一页</a>
{% else %} {% else %}
<a href="?page=1">&laquo; 首页</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page=1">&laquo; 首页</a>
<a href="?page={{ page_obj.previous_page_number }}">上一页</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
@@ -145,15 +221,15 @@
<a href="#" class="current">{{ num }}</a> <a href="#" class="current">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
{% if selected_website %} {% if selected_website %}
<a href="?website={{ selected_website.id }}&page={{ num }}">{{ num }}</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ num }}">{{ num }}</a>
{% else %} {% else %}
<a href="?page={{ num }}">{{ num }}</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ num }}">{{ num }}</a>
{% endif %} {% endif %}
{% elif num == 1 or num == paginator.num_pages %} {% elif num == 1 or num == paginator.num_pages %}
{% if selected_website %} {% if selected_website %}
<a href="?website={{ selected_website.id }}&page={{ num }}">{{ num }}</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ num }}">{{ num }}</a>
{% else %} {% else %}
<a href="?page={{ num }}">{{ num }}</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ num }}">{{ num }}</a>
{% endif %} {% endif %}
{% elif num == page_obj.number|add:'-3' or num == page_obj.number|add:'3' %} {% elif num == page_obj.number|add:'-3' or num == page_obj.number|add:'3' %}
<span class="ellipsis">...</span> <span class="ellipsis">...</span>
@@ -163,14 +239,14 @@
{% if page_obj.has_next %} {% if page_obj.has_next %}
{% if selected_website %} {% if selected_website %}
<a href="?website={{ selected_website.id }}&page={{ page_obj.next_page_number }}">下一页</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ page_obj.next_page_number }}">下一页</a>
<a href="?website={{ selected_website.id }}&page={{ page_obj.paginator.num_pages }}">末页 &raquo;</a> <a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ page_obj.paginator.num_pages }}">末页 &raquo;</a>
{% else %} {% else %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.next_page_number }}">下一页</a>
<a href="?page={{ page_obj.paginator.num_pages }}">末页 &raquo;</a> <a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.paginator.num_pages }}">末页 &raquo;</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -1,28 +1,44 @@
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render
from django.core.paginator import Paginator from django.core.paginator import Paginator
from .models import Article from .models import Article, Website
def article_list(request): def article_list(request):
""" # 获取所有启用的网站
显示文章列表的视图函数 websites = Website.objects.filter(enabled=True)
"""
articles = Article.objects.all().order_by('-created_at')
paginator = Paginator(articles, 20) # 每页显示10篇文章
# 获取筛选网站
selected_website = None
articles = Article.objects.all()
website_id = request.GET.get('website')
if website_id:
try:
selected_website = Website.objects.get(id=website_id)
articles = articles.filter(website=selected_website)
except Website.DoesNotExist:
pass
# 新增:处理关键词搜索
search_query = request.GET.get('q')
if search_query:
articles = articles.filter(title__icontains=search_query)
# 按创建时间倒序排列
articles = articles.order_by('-created_at')
# 分页
paginator = Paginator(articles, 10) # 每页显示10篇文章
page_number = request.GET.get('page') page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
return render(request, 'core/article_list.html', { return render(request, 'core/article_list.html', {
'page_obj': page_obj 'page_obj': page_obj,
'websites': websites,
'selected_website': selected_website,
# 新增:传递搜索关键词到模板
'search_query': search_query
}) })
def article_detail(request, article_id): def article_detail(request, article_id):
""" article = Article.objects.get(id=article_id)
显示文章详情的视图函数 return render(request, 'core/article_detail.html', {'article': article})
"""
article = get_object_or_404(Article, id=article_id)
return render(request, 'core/article_detail.html', {
'article': article
})

31
requirements.txt Normal file
View File

@@ -0,0 +1,31 @@
asgiref==3.9.1
asttokens==3.0.0
beautifulsoup4==4.13.4
bs4==0.0.2
certifi==2025.8.3
charset-normalizer==3.4.3
decorator==5.2.1
Django==5.1
executing==2.2.0
idna==3.10
ipython==9.4.0
ipython_pygments_lexers==1.1.1
jedi==0.19.2
lxml==6.0.0
matplotlib-inline==0.1.7
parso==0.8.4
pexpect==4.9.0
prompt_toolkit==3.0.51
ptyprocess==0.7.0
pure_eval==0.2.3
Pygments==2.19.2
python-docx==1.2.0
requests==2.32.4
soupsieve==2.7
sqlparse==0.5.3
stack-data==0.6.3
traitlets==5.14.3
typing_extensions==4.14.1
urllib3==2.5.0
uv==0.8.8
wcwidth==0.2.13