Add Search button
This commit is contained in:
112
core/admin.py
112
core/admin.py
@@ -9,33 +9,39 @@ 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):
|
||||||
"""一键删除东方烟草报的所有文章"""
|
"""一键删除东方烟草报的所有文章"""
|
||||||
# 获取东方烟草报网站对象
|
# 获取东方烟草报网站对象
|
||||||
@@ -46,33 +52,34 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
self.message_user(request, f"成功删除 {deleted_count} 篇东方烟草报文章", messages.SUCCESS)
|
self.message_user(request, f"成功删除 {deleted_count} 篇东方烟草报文章", messages.SUCCESS)
|
||||||
except Website.DoesNotExist:
|
except Website.DoesNotExist:
|
||||||
self.message_user(request, "未找到东方烟草报网站配置", messages.ERROR)
|
self.message_user(request, "未找到东方烟草报网站配置", messages.ERROR)
|
||||||
|
|
||||||
# 设置动作的显示名称
|
# 设置动作的显示名称
|
||||||
delete_dongfangyancao_articles.short_description = "删除所有东方烟草报文章"
|
delete_dongfangyancao_articles.short_description = "删除所有东方烟草报文章"
|
||||||
|
|
||||||
def export_as_csv(self, request, queryset):
|
def export_as_csv(self, request, queryset):
|
||||||
"""导出选中的文章为CSV格式"""
|
"""导出选中的文章为CSV格式"""
|
||||||
meta = self.model._meta
|
meta = self.model._meta
|
||||||
field_names = [field.name for field in meta.fields]
|
field_names = [field.name for field in meta.fields]
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(content_type='text/csv')
|
||||||
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
export_as_csv.short_description = "导出选中文章为CSV格式"
|
export_as_csv.short_description = "导出选中文章为CSV格式"
|
||||||
|
|
||||||
def export_as_json(self, request, queryset):
|
def export_as_json(self, request, queryset):
|
||||||
"""导出选中的文章为JSON格式"""
|
"""导出选中的文章为JSON格式"""
|
||||||
response = HttpResponse(content_type='application/json')
|
response = HttpResponse(content_type='application/json')
|
||||||
response['Content-Disposition'] = 'attachment; filename=articles.json'
|
response['Content-Disposition'] = 'attachment; filename=articles.json'
|
||||||
|
|
||||||
# 构造要导出的数据
|
# 构造要导出的数据
|
||||||
articles_data = []
|
articles_data = []
|
||||||
for article in queryset:
|
for article in queryset:
|
||||||
@@ -86,11 +93,11 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'media_files': article.media_files
|
'media_files': article.media_files
|
||||||
})
|
})
|
||||||
|
|
||||||
# 写入JSON数据
|
# 写入JSON数据
|
||||||
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
export_as_json.short_description = "导出选中文章为JSON格式"
|
export_as_json.short_description = "导出选中文章为JSON格式"
|
||||||
|
|
||||||
def export_as_word(self, request, queryset):
|
def export_as_word(self, request, queryset):
|
||||||
@@ -110,19 +117,20 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
for article in queryset:
|
for article in queryset:
|
||||||
# 添加文章标题
|
# 添加文章标题
|
||||||
doc.add_heading(article.title, level=1)
|
doc.add_heading(article.title, level=1)
|
||||||
|
|
||||||
# 添加文章元数据
|
# 添加文章元数据
|
||||||
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')}")
|
||||||
|
|
||||||
# 添加文章内容
|
# 添加文章内容
|
||||||
doc.add_heading('内容', level=2)
|
doc.add_heading('内容', level=2)
|
||||||
# 简单处理HTML内容,移除标签并处理图片
|
# 简单处理HTML内容,移除标签并处理图片
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
soup = BeautifulSoup(article.content, 'html.parser')
|
soup = BeautifulSoup(article.content, 'html.parser')
|
||||||
|
|
||||||
# 处理内容中的图片
|
# 处理内容中的图片
|
||||||
for img in soup.find_all('img'):
|
for img in soup.find_all('img'):
|
||||||
src = img.get('src', '')
|
src = img.get('src', '')
|
||||||
@@ -133,7 +141,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import requests
|
import requests
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
# 构建完整的图片路径
|
# 构建完整的图片路径
|
||||||
if src.startswith('http'):
|
if src.startswith('http'):
|
||||||
# 网络图片
|
# 网络图片
|
||||||
@@ -148,13 +156,13 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果添加图片失败,添加图片URL作为文本
|
# 如果添加图片失败,添加图片URL作为文本
|
||||||
doc.add_paragraph(f"[图片: {src}]")
|
doc.add_paragraph(f"[图片: {src}]")
|
||||||
|
|
||||||
# 移除原始img标签
|
# 移除原始img标签
|
||||||
img.decompose()
|
img.decompose()
|
||||||
|
|
||||||
content_text = soup.get_text()
|
content_text = soup.get_text()
|
||||||
doc.add_paragraph(content_text)
|
doc.add_paragraph(content_text)
|
||||||
|
|
||||||
# 添加媒体文件信息
|
# 添加媒体文件信息
|
||||||
if article.media_files:
|
if article.media_files:
|
||||||
doc.add_heading('媒体文件', level=2)
|
doc.add_heading('媒体文件', level=2)
|
||||||
@@ -164,7 +172,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
# 添加图片到文档
|
# 添加图片到文档
|
||||||
@@ -179,44 +187,46 @@ class ArticleAdmin(admin.ModelAdmin):
|
|||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
|
|
||||||
# 添加分页符
|
# 添加分页符
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
# 保存到内存
|
# 保存到内存
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
doc.save(buffer)
|
doc.save(buffer)
|
||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
# 创建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')
|
||||||
search_fields = ('title', 'content')
|
search_fields = ('title', 'content')
|
||||||
list_filter = ('pub_date',)
|
list_filter = ('pub_date',)
|
||||||
actions = ['export_as_csv', 'export_as_json']
|
actions = ['export_as_csv', 'export_as_json']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
# 只显示新华网的文章
|
# 只显示新华网的文章
|
||||||
return qs.filter(website__name='www.news.cn')
|
return qs.filter(website__name='www.news.cn')
|
||||||
|
|
||||||
def export_as_csv(self, request, queryset):
|
def export_as_csv(self, request, queryset):
|
||||||
"""导出选中的文章为CSV格式"""
|
"""导出选中的文章为CSV格式"""
|
||||||
meta = self.model._meta
|
meta = self.model._meta
|
||||||
field_names = [field.name for field in meta.fields if field.name != 'content'] # 排除content字段以减小CSV大小
|
field_names = [field.name for field in meta.fields if field.name != 'content'] # 排除content字段以减小CSV大小
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(content_type='text/csv')
|
||||||
response['Content-Disposition'] = 'attachment; filename=news_cn_articles.csv'
|
response['Content-Disposition'] = 'attachment; filename=news_cn_articles.csv'
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
|
|
||||||
writer.writerow(field_names)
|
writer.writerow(field_names)
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
row = []
|
row = []
|
||||||
@@ -228,16 +238,16 @@ class NewsCnArticleAdmin(admin.ModelAdmin):
|
|||||||
value = value.name
|
value = value.name
|
||||||
row.append(value)
|
row.append(value)
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
export_as_csv.short_description = "导出选中文章为CSV格式"
|
export_as_csv.short_description = "导出选中文章为CSV格式"
|
||||||
|
|
||||||
def export_as_json(self, request, queryset):
|
def export_as_json(self, request, queryset):
|
||||||
"""导出选中的文章为JSON格式"""
|
"""导出选中的文章为JSON格式"""
|
||||||
response = HttpResponse(content_type='application/json')
|
response = HttpResponse(content_type='application/json')
|
||||||
response['Content-Disposition'] = 'attachment; filename=news_cn_articles.json'
|
response['Content-Disposition'] = 'attachment; filename=news_cn_articles.json'
|
||||||
|
|
||||||
# 构造要导出的数据
|
# 构造要导出的数据
|
||||||
articles_data = []
|
articles_data = []
|
||||||
for article in queryset:
|
for article in queryset:
|
||||||
@@ -251,43 +261,44 @@ class NewsCnArticleAdmin(admin.ModelAdmin):
|
|||||||
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'media_files': article.media_files
|
'media_files': article.media_files
|
||||||
})
|
})
|
||||||
|
|
||||||
# 写入JSON数据
|
# 写入JSON数据
|
||||||
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
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')
|
||||||
list_filter = ('pub_date',)
|
list_filter = ('pub_date',)
|
||||||
# 添加动作选项
|
# 添加动作选项
|
||||||
actions = ['delete_selected_articles', 'delete_all_articles', 'export_as_csv', 'export_as_json']
|
actions = ['delete_selected_articles', 'delete_all_articles', 'export_as_csv', 'export_as_json']
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super().get_queryset(request)
|
qs = super().get_queryset(request)
|
||||||
# 只显示东方烟草报的文章
|
# 只显示东方烟草报的文章
|
||||||
return qs.filter(website__name='东方烟草报')
|
return qs.filter(website__name='东方烟草报')
|
||||||
|
|
||||||
def delete_all_articles(self, request, queryset):
|
def delete_all_articles(self, request, queryset):
|
||||||
"""删除当前筛选的所有文章(东方烟草报的所有文章)"""
|
"""删除当前筛选的所有文章(东方烟草报的所有文章)"""
|
||||||
# 删除所有东方烟草报的文章
|
# 删除所有东方烟草报的文章
|
||||||
deleted_count = self.get_queryset(request).delete()[0]
|
deleted_count = self.get_queryset(request).delete()[0]
|
||||||
self.message_user(request, f"成功删除 {deleted_count} 篇文章", messages.SUCCESS)
|
self.message_user(request, f"成功删除 {deleted_count} 篇文章", messages.SUCCESS)
|
||||||
|
|
||||||
# 设置动作的显示名称
|
# 设置动作的显示名称
|
||||||
delete_all_articles.short_description = "删除所有当前筛选的文章"
|
delete_all_articles.short_description = "删除所有当前筛选的文章"
|
||||||
|
|
||||||
def export_as_csv(self, request, queryset):
|
def export_as_csv(self, request, queryset):
|
||||||
"""导出选中的文章为CSV格式"""
|
"""导出选中的文章为CSV格式"""
|
||||||
meta = self.model._meta
|
meta = self.model._meta
|
||||||
field_names = [field.name for field in meta.fields if field.name != 'content'] # 排除content字段以减小CSV大小
|
field_names = [field.name for field in meta.fields if field.name != 'content'] # 排除content字段以减小CSV大小
|
||||||
|
|
||||||
response = HttpResponse(content_type='text/csv')
|
response = HttpResponse(content_type='text/csv')
|
||||||
response['Content-Disposition'] = 'attachment; filename=dongfangyancao_articles.csv'
|
response['Content-Disposition'] = 'attachment; filename=dongfangyancao_articles.csv'
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
|
|
||||||
writer.writerow(field_names)
|
writer.writerow(field_names)
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
row = []
|
row = []
|
||||||
@@ -299,16 +310,16 @@ class DongfangyancaoArticleAdmin(admin.ModelAdmin):
|
|||||||
value = value.name
|
value = value.name
|
||||||
row.append(value)
|
row.append(value)
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
export_as_csv.short_description = "导出选中文章为CSV格式"
|
export_as_csv.short_description = "导出选中文章为CSV格式"
|
||||||
|
|
||||||
def export_as_json(self, request, queryset):
|
def export_as_json(self, request, queryset):
|
||||||
"""导出选中的文章为JSON格式"""
|
"""导出选中的文章为JSON格式"""
|
||||||
response = HttpResponse(content_type='application/json')
|
response = HttpResponse(content_type='application/json')
|
||||||
response['Content-Disposition'] = 'attachment; filename=dongfangyancao_articles.json'
|
response['Content-Disposition'] = 'attachment; filename=dongfangyancao_articles.json'
|
||||||
|
|
||||||
# 构造要导出的数据
|
# 构造要导出的数据
|
||||||
articles_data = []
|
articles_data = []
|
||||||
for article in queryset:
|
for article in queryset:
|
||||||
@@ -322,16 +333,17 @@ class DongfangyancaoArticleAdmin(admin.ModelAdmin):
|
|||||||
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
'created_at': article.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
'media_files': article.media_files
|
'media_files': article.media_files
|
||||||
})
|
})
|
||||||
|
|
||||||
# 写入JSON数据
|
# 写入JSON数据
|
||||||
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
response.write(json.dumps(articles_data, ensure_ascii=False, indent=2))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
dongfangyancao_admin.register(Website, WebsiteAdmin)
|
dongfangyancao_admin.register(Website, WebsiteAdmin)
|
||||||
dongfangyancao_admin.register(Article, DongfangyancaoArticleAdmin)
|
dongfangyancao_admin.register(Article, DongfangyancaoArticleAdmin)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -18,4 +17,4 @@ class Command(BaseCommand):
|
|||||||
start_url = "https://www.eastobacco.com/"
|
start_url = "https://www.eastobacco.com/"
|
||||||
self.stdout.write(f"开始全站爬取: {start_url}")
|
self.stdout.write(f"开始全站爬取: {start_url}")
|
||||||
full_site_crawler(start_url, website, max_pages=500)
|
full_site_crawler(start_url, website, max_pages=500)
|
||||||
self.stdout.write("爬取完成")
|
self.stdout.write("爬取完成")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Command(BaseCommand):
|
|||||||
website_name = options['website']
|
website_name = options['website']
|
||||||
output_path = options['output']
|
output_path = options['output']
|
||||||
include_media = options['include_media']
|
include_media = options['include_media']
|
||||||
|
|
||||||
# 获取文章查询集
|
# 获取文章查询集
|
||||||
articles = Article.objects.all()
|
articles = Article.objects.all()
|
||||||
if website_name:
|
if website_name:
|
||||||
@@ -33,15 +33,15 @@ class Command(BaseCommand):
|
|||||||
except Website.DoesNotExist:
|
except Website.DoesNotExist:
|
||||||
self.stdout.write(self.style.ERROR(f'网站 "{website_name}" 不存在'))
|
self.stdout.write(self.style.ERROR(f'网站 "{website_name}" 不存在'))
|
||||||
return
|
return
|
||||||
|
|
||||||
if not articles.exists():
|
if not articles.exists():
|
||||||
self.stdout.write(self.style.WARNING('没有找到文章'))
|
self.stdout.write(self.style.WARNING('没有找到文章'))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 准备导出数据
|
# 准备导出数据
|
||||||
articles_data = []
|
articles_data = []
|
||||||
media_files = []
|
media_files = []
|
||||||
|
|
||||||
for article in articles:
|
for article in articles:
|
||||||
article_data = {
|
article_data = {
|
||||||
'id': article.id,
|
'id': article.id,
|
||||||
@@ -54,14 +54,14 @@ class Command(BaseCommand):
|
|||||||
'media_files': article.media_files
|
'media_files': article.media_files
|
||||||
}
|
}
|
||||||
articles_data.append(article_data)
|
articles_data.append(article_data)
|
||||||
|
|
||||||
# 收集媒体文件路径
|
# 收集媒体文件路径
|
||||||
if include_media:
|
if include_media:
|
||||||
for media_path in article.media_files:
|
for media_path in article.media_files:
|
||||||
full_path = os.path.join(settings.MEDIA_ROOT, media_path)
|
full_path = os.path.join(settings.MEDIA_ROOT, media_path)
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
media_files.append(full_path)
|
media_files.append(full_path)
|
||||||
|
|
||||||
# 确定输出路径
|
# 确定输出路径
|
||||||
if not output_path:
|
if not output_path:
|
||||||
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
|
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
|
||||||
@@ -69,7 +69,7 @@ class Command(BaseCommand):
|
|||||||
output_path = f'articles_export_{timestamp}.zip'
|
output_path = f'articles_export_{timestamp}.zip'
|
||||||
else:
|
else:
|
||||||
output_path = f'articles_export_{timestamp}.{format_type}'
|
output_path = f'articles_export_{timestamp}.{format_type}'
|
||||||
|
|
||||||
# 执行导出
|
# 执行导出
|
||||||
if include_media:
|
if include_media:
|
||||||
self.export_with_media(articles_data, media_files, output_path, format_type)
|
self.export_with_media(articles_data, media_files, output_path, format_type)
|
||||||
@@ -84,7 +84,7 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.ERROR('不支持的格式,仅支持 json、csv 或 docx'))
|
self.stdout.write(self.style.ERROR('不支持的格式,仅支持 json、csv 或 docx'))
|
||||||
return
|
return
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'成功导出 {len(articles_data)} 篇文章到 {output_path}'))
|
self.stdout.write(self.style.SUCCESS(f'成功导出 {len(articles_data)} 篇文章到 {output_path}'))
|
||||||
|
|
||||||
def export_as_json(self, articles_data, output_path):
|
def export_as_json(self, articles_data, output_path):
|
||||||
@@ -94,16 +94,17 @@ class Command(BaseCommand):
|
|||||||
def export_as_csv(self, articles_data, output_path):
|
def export_as_csv(self, articles_data, output_path):
|
||||||
if not articles_data:
|
if not articles_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 打开CSV文件
|
# 打开CSV文件
|
||||||
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
||||||
fieldnames = ['id', 'title', 'website', 'url', 'pub_date', 'content', 'created_at', 'media_files']
|
fieldnames = ['id', 'title', 'website', 'url', 'pub_date', 'content', 'created_at', 'media_files']
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||||
|
|
||||||
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格式导出方法
|
||||||
@@ -122,19 +123,19 @@ class Command(BaseCommand):
|
|||||||
for article_data in articles_data:
|
for article_data in articles_data:
|
||||||
# 添加文章标题
|
# 添加文章标题
|
||||||
doc.add_heading(article_data['title'], level=1)
|
doc.add_heading(article_data['title'], level=1)
|
||||||
|
|
||||||
# 添加文章元数据
|
# 添加文章元数据
|
||||||
doc.add_paragraph(f"网站: {article_data['website']}")
|
doc.add_paragraph(f"网站: {article_data['website']}")
|
||||||
doc.add_paragraph(f"URL: {article_data['url']}")
|
doc.add_paragraph(f"URL: {article_data['url']}")
|
||||||
doc.add_paragraph(f"发布时间: {article_data['pub_date']}")
|
doc.add_paragraph(f"发布时间: {article_data['pub_date']}")
|
||||||
doc.add_paragraph(f"创建时间: {article_data['created_at']}")
|
doc.add_paragraph(f"创建时间: {article_data['created_at']}")
|
||||||
|
|
||||||
# 添加文章内容
|
# 添加文章内容
|
||||||
doc.add_heading('内容', level=2)
|
doc.add_heading('内容', level=2)
|
||||||
# 简单处理HTML内容,移除标签
|
# 简单处理HTML内容,移除标签
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
soup = BeautifulSoup(article_data['content'], 'html.parser')
|
soup = BeautifulSoup(article_data['content'], 'html.parser')
|
||||||
|
|
||||||
# 处理内容中的图片
|
# 处理内容中的图片
|
||||||
for img in soup.find_all('img'):
|
for img in soup.find_all('img'):
|
||||||
src = img.get('src', '')
|
src = img.get('src', '')
|
||||||
@@ -145,7 +146,7 @@ class Command(BaseCommand):
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import requests
|
import requests
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
# 构建完整的图片路径
|
# 构建完整的图片路径
|
||||||
if src.startswith('http'):
|
if src.startswith('http'):
|
||||||
# 网络图片
|
# 网络图片
|
||||||
@@ -160,13 +161,13 @@ class Command(BaseCommand):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果添加图片失败,添加图片URL作为文本
|
# 如果添加图片失败,添加图片URL作为文本
|
||||||
doc.add_paragraph(f"[图片: {src}]")
|
doc.add_paragraph(f"[图片: {src}]")
|
||||||
|
|
||||||
# 移除原始img标签
|
# 移除原始img标签
|
||||||
img.decompose()
|
img.decompose()
|
||||||
|
|
||||||
content_text = soup.get_text()
|
content_text = soup.get_text()
|
||||||
doc.add_paragraph(content_text)
|
doc.add_paragraph(content_text)
|
||||||
|
|
||||||
# 添加媒体文件信息
|
# 添加媒体文件信息
|
||||||
if article_data['media_files']:
|
if article_data['media_files']:
|
||||||
doc.add_heading('媒体文件', level=2)
|
doc.add_heading('媒体文件', level=2)
|
||||||
@@ -176,7 +177,7 @@ class Command(BaseCommand):
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
# 添加图片到文档
|
# 添加图片到文档
|
||||||
@@ -191,10 +192,10 @@ class Command(BaseCommand):
|
|||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
|
|
||||||
# 添加分页符
|
# 添加分页符
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
# 保存文档
|
# 保存文档
|
||||||
doc.save(output_path)
|
doc.save(output_path)
|
||||||
|
|
||||||
@@ -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格式支持
|
||||||
@@ -225,7 +227,7 @@ class Command(BaseCommand):
|
|||||||
from docx import Document
|
from docx import Document
|
||||||
from docx.shared import Inches
|
from docx.shared import Inches
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
doc = Document()
|
doc = Document()
|
||||||
doc.add_heading('文章导出', 0)
|
doc.add_heading('文章导出', 0)
|
||||||
|
|
||||||
@@ -235,11 +237,11 @@ class Command(BaseCommand):
|
|||||||
doc.add_paragraph(f"URL: {article_data['url']}")
|
doc.add_paragraph(f"URL: {article_data['url']}")
|
||||||
doc.add_paragraph(f"发布时间: {article_data['pub_date']}")
|
doc.add_paragraph(f"发布时间: {article_data['pub_date']}")
|
||||||
doc.add_paragraph(f"创建时间: {article_data['created_at']}")
|
doc.add_paragraph(f"创建时间: {article_data['created_at']}")
|
||||||
|
|
||||||
doc.add_heading('内容', level=2)
|
doc.add_heading('内容', level=2)
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
soup = BeautifulSoup(article_data['content'], 'html.parser')
|
soup = BeautifulSoup(article_data['content'], 'html.parser')
|
||||||
|
|
||||||
# 处理内容中的图片
|
# 处理内容中的图片
|
||||||
for img in soup.find_all('img'):
|
for img in soup.find_all('img'):
|
||||||
src = img.get('src', '')
|
src = img.get('src', '')
|
||||||
@@ -249,7 +251,7 @@ class Command(BaseCommand):
|
|||||||
import os
|
import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# 构建完整的图片路径
|
# 构建完整的图片路径
|
||||||
if src.startswith('http'):
|
if src.startswith('http'):
|
||||||
# 网络图片
|
# 网络图片
|
||||||
@@ -264,20 +266,20 @@ class Command(BaseCommand):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果添加图片失败,添加图片URL作为文本
|
# 如果添加图片失败,添加图片URL作为文本
|
||||||
doc.add_paragraph(f"[图片: {src}]")
|
doc.add_paragraph(f"[图片: {src}]")
|
||||||
|
|
||||||
# 移除原始img标签
|
# 移除原始img标签
|
||||||
img.decompose()
|
img.decompose()
|
||||||
|
|
||||||
content_text = soup.get_text()
|
content_text = soup.get_text()
|
||||||
doc.add_paragraph(content_text)
|
doc.add_paragraph(content_text)
|
||||||
|
|
||||||
if article_data['media_files']:
|
if article_data['media_files']:
|
||||||
doc.add_heading('媒体文件', level=2)
|
doc.add_heading('媒体文件', level=2)
|
||||||
for media_file in article_data['media_files']:
|
for media_file in article_data['media_files']:
|
||||||
try:
|
try:
|
||||||
import os
|
import os
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
full_path = os.path.join(settings.MEDIA_ROOT, media_file)
|
||||||
if os.path.exists(full_path):
|
if os.path.exists(full_path):
|
||||||
# 添加图片到文档
|
# 添加图片到文档
|
||||||
@@ -292,9 +294,9 @@ class Command(BaseCommand):
|
|||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc.add_paragraph(media_file)
|
doc.add_paragraph(media_file)
|
||||||
|
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
# 将文档保存到内存中再写入ZIP
|
# 将文档保存到内存中再写入ZIP
|
||||||
doc_buffer = BytesIO()
|
doc_buffer = BytesIO()
|
||||||
doc.save(doc_buffer)
|
doc.save(doc_buffer)
|
||||||
@@ -302,8 +304,8 @@ class Command(BaseCommand):
|
|||||||
zipf.writestr(data_filename, doc_buffer.read())
|
zipf.writestr(data_filename, doc_buffer.read())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
zipf.writestr(data_filename, "错误:缺少python-docx库,无法生成Word文档")
|
zipf.writestr(data_filename, "错误:缺少python-docx库,无法生成Word文档")
|
||||||
|
|
||||||
# 添加媒体文件
|
# 添加媒体文件
|
||||||
for media_path in media_files:
|
for media_path in media_files:
|
||||||
arcname = os.path.join('media', os.path.relpath(media_path, settings.MEDIA_ROOT))
|
arcname = os.path.join('media', os.path.relpath(media_path, settings.MEDIA_ROOT))
|
||||||
zipf.write(media_path, arcname)
|
zipf.write(media_path, arcname)
|
||||||
|
|||||||
@@ -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,23 +62,24 @@
|
|||||||
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>
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<div class="content">
|
|
||||||
{{ article.content|safe }}
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<p><a href="{% url 'article_list' %}" class="back-link">← 返回列表</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<div class="content">
|
||||||
|
{{ article.content|safe }}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<p><a href="{% url 'article_list' %}" class="back-link">← 返回列表</a></p>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -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,95 +95,158 @@
|
|||||||
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 class="filters">
|
<!-- 新增:返回首页链接 -->
|
||||||
<strong>按网站筛选:</strong>
|
<div style="margin-bottom: 20px;">
|
||||||
<a href="{% url 'article_list' %}" {% if not selected_website %}class="active"{% endif %}>全部</a>
|
<a href="{% url 'article_list' %}" style="color: #3498db; text-decoration: none;">← 返回首页</a>
|
||||||
{% for website in websites %}
|
|
||||||
<a href="?website={{ website.id }}" {% if selected_website.id == website.id %}class="active"{% endif %}>{{ website.name }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for article in page_obj %}
|
|
||||||
<li>
|
|
||||||
<a href="{% url 'article_detail' article.id %}">{{ article.title }}</a>
|
|
||||||
<div class="meta">({{ article.website.name }} - {{ article.created_at|date:"Y-m-d" }})</div>
|
|
||||||
</li>
|
|
||||||
{% empty %}
|
|
||||||
<li>暂无文章</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="pagination">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
{% if selected_website %}
|
|
||||||
<a href="?website={{ selected_website.id }}&page=1">« 首页</a>
|
|
||||||
<a href="?website={{ selected_website.id }}&page={{ page_obj.previous_page_number }}">上一页</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page=1">« 首页</a>
|
|
||||||
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<span>第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页</span>
|
|
||||||
|
|
||||||
<!-- 修改:优化页码显示逻辑 -->
|
|
||||||
{% with page_obj.paginator as paginator %}
|
|
||||||
{% for num in paginator.page_range %}
|
|
||||||
{% if page_obj.number == num %}
|
|
||||||
<a href="#" class="current">{{ num }}</a>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
{% if selected_website %}
|
|
||||||
<a href="?website={{ selected_website.id }}&page={{ num }}">{{ num }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page={{ num }}">{{ num }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% elif num == 1 or num == paginator.num_pages %}
|
|
||||||
{% if selected_website %}
|
|
||||||
<a href="?website={{ selected_website.id }}&page={{ num }}">{{ num }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page={{ num }}">{{ num }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% elif num == page_obj.number|add:'-3' or num == page_obj.number|add:'3' %}
|
|
||||||
<span class="ellipsis">...</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
{% if selected_website %}
|
|
||||||
<a href="?website={{ selected_website.id }}&page={{ page_obj.next_page_number }}">下一页</a>
|
|
||||||
<a href="?website={{ selected_website.id }}&page={{ page_obj.paginator.num_pages }}">末页 »</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
|
|
||||||
<a href="?page={{ page_obj.paginator.num_pages }}">末页 »</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
||||||
|
<strong>按网站筛选:</strong>
|
||||||
|
<a href="{% url 'article_list' %}{% if search_query %}?q={{ search_query }}{% endif %}" {% if not selected_website %}class="active" {% endif %}>全部</a>
|
||||||
|
{% for website in websites %}
|
||||||
|
<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 %}
|
||||||
|
</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>
|
||||||
|
{% for article in page_obj %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'article_detail' article.id %}">{{ article.title }}</a>
|
||||||
|
<div class="meta">({{ article.website.name }} - {{ article.created_at|date:"Y-m-d" }})</div>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li>暂无文章</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
{% if selected_website %}
|
||||||
|
<a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page=1">« 首页</a>
|
||||||
|
<a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ page_obj.previous_page_number }}">上一页</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page=1">« 首页</a>
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.previous_page_number }}">上一页</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span>第 {{ page_obj.number }} 页,共 {{ page_obj.paginator.num_pages }} 页</span>
|
||||||
|
|
||||||
|
<!-- 修改:优化页码显示逻辑 -->
|
||||||
|
{% with page_obj.paginator as paginator %}
|
||||||
|
{% for num in paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<a href="#" class="current">{{ num }}</a>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
{% if selected_website %}
|
||||||
|
<a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ num }}">{{ num }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ num }}">{{ num }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% elif num == 1 or num == paginator.num_pages %}
|
||||||
|
{% if selected_website %}
|
||||||
|
<a href="?website={{ selected_website.id }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ num }}">{{ num }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ num }}">{{ num }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% elif num == page_obj.number|add:'-3' or num == page_obj.number|add:'3' %}
|
||||||
|
<span class="ellipsis">...</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
{% if selected_website %}
|
||||||
|
<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 }}{% if search_query %}&q={{ search_query }}{% endif %}&page={{ page_obj.paginator.num_pages }}">末页 »</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.next_page_number }}">下一页</a>
|
||||||
|
<a href="?{% if search_query %}q={{ search_query }}&{% endif %}page={{ page_obj.paginator.num_pages }}">末页 »</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -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
31
requirements.txt
Normal 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
|
||||||
Reference in New Issue
Block a user