插件优化&&文章推荐

pull/5/head
liangliangyy 5 months ago
parent 7bdc119371
commit d21bd5ffc5

@ -2735,6 +2735,76 @@ li #reply-title {
.comment-content .codehilite .nf,
.comment-content .codehilite .n,
.comment-content .codehilite .p,
.comment-body .codehilite .kt,
.comment-body .codehilite .nf,
.comment-body .codehilite .n,
.comment-body .codehilite .p {
word-wrap: break-word !important;
overflow-wrap: break-word !important;
}
/* 搜索结果高亮样式 */
.search-result {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e1e1e1;
border-radius: 5px;
background: #fff;
}
.search-result .entry-title {
margin: 0 0 10px 0;
font-size: 1.5em;
}
.search-result .entry-title a {
color: #2c3e50;
text-decoration: none;
}
.search-result .entry-title a:hover {
color: #3498db;
}
.search-result .entry-meta {
color: #7f8c8d;
font-size: 0.9em;
margin-bottom: 15px;
}
.search-result .entry-meta span {
margin-right: 15px;
}
.search-excerpt {
line-height: 1.6;
color: #555;
}
.search-excerpt p {
margin: 10px 0;
}
/* 搜索关键词高亮 */
.search-excerpt em,
.search-result .entry-title em {
background-color: #fff3cd;
color: #856404;
font-style: normal;
font-weight: bold;
padding: 2px 4px;
border-radius: 3px;
}
.more-link {
color: #3498db;
text-decoration: none;
font-weight: bold;
}
.more-link:hover {
text-decoration: underline;
}
.comment-content .codehilite .w,
.comment-content .codehilite .o,
.comment-body .codehilite .kt,

@ -421,3 +421,134 @@ def query(qs, **kwargs):
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
# === 插件系统模板标签 ===
@register.simple_tag(takes_context=True)
def render_plugin_widgets(context, position, **kwargs):
"""
渲染指定位置的所有插件组件
Args:
context: 模板上下文
position: 位置标识
**kwargs: 传递给插件的额外参数
Returns:
按优先级排序的所有插件HTML内容
"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
widgets = []
for plugin in get_loaded_plugins():
try:
widget_data = plugin.render_position_widget(
position=position,
context=context,
**kwargs
)
if widget_data:
widgets.append(widget_data)
except Exception as e:
logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}")
# 按优先级排序(数字越小优先级越高)
widgets.sort(key=lambda x: x['priority'])
# 合并HTML内容
html_parts = [widget['html'] for widget in widgets]
return mark_safe(''.join(html_parts))
@register.simple_tag(takes_context=True)
def plugin_head_resources(context):
"""渲染所有插件的head资源仅自定义HTMLCSS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义head HTMLCSS文件已通过压缩系统处理
head_html = plugin.get_head_html(context)
if head_html:
resources.append(head_html)
except Exception as e:
logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.simple_tag(takes_context=True)
def plugin_body_resources(context):
"""渲染所有插件的body资源仅自定义HTMLJS已集成到压缩系统"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
resources = []
for plugin in get_loaded_plugins():
try:
# 只处理自定义body HTMLJS文件已通过压缩系统处理
body_html = plugin.get_body_html(context)
if body_html:
resources.append(body_html)
except Exception as e:
logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}")
return mark_safe('\n'.join(resources))
@register.inclusion_tag('plugins/css_includes.html')
def plugin_compressed_css():
"""插件CSS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
css_files = []
for plugin in get_loaded_plugins():
for css_file in plugin.get_css_files():
css_url = plugin.get_static_url(css_file)
css_files.append(css_url)
return {'css_files': css_files}
@register.inclusion_tag('plugins/js_includes.html')
def plugin_compressed_js():
"""插件JS压缩包含模板"""
from djangoblog.plugin_manage.loader import get_loaded_plugins
js_files = []
for plugin in get_loaded_plugins():
for js_file in plugin.get_js_files():
js_url = plugin.get_static_url(js_file)
js_files.append(js_url)
return {'js_files': js_files}
@register.simple_tag(takes_context=True)
def plugin_widget(context, plugin_name, widget_type='default', **kwargs):
"""
渲染指定插件的组件
使用方式
{% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %}
"""
from djangoblog.plugin_manage.loader import get_plugin_by_slug
plugin = get_plugin_by_slug(plugin_name)
if plugin and hasattr(plugin, 'render_template'):
try:
widget_context = {**context.flatten(), **kwargs}
template_name = f"{widget_type}.html"
return mark_safe(plugin.render_template(template_name, widget_context))
except Exception as e:
logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}")
return ""

@ -152,6 +152,11 @@ class ArticleDetailView(DetailView):
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 触发文章详情加载钩子,让插件可以添加额外的上下文数据
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
return context

@ -1,4 +1,8 @@
import logging
from pathlib import Path
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
@ -8,13 +12,34 @@ class BasePlugin:
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
PLUGIN_AUTHOR = None
# 插件配置
SUPPORTED_POSITIONS = [] # 支持的显示位置
DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
# 设置插件路径
self.plugin_dir = self._get_plugin_directory()
self.plugin_slug = self._get_plugin_slug()
self.init_plugin()
self.register_hooks()
def _get_plugin_directory(self):
"""获取插件目录路径"""
import inspect
plugin_file = inspect.getfile(self.__class__)
return Path(plugin_file).parent
def _get_plugin_slug(self):
"""获取插件标识符(目录名)"""
return self.plugin_dir.name
def init_plugin(self):
"""
插件初始化逻辑
@ -29,6 +54,129 @@ class BasePlugin:
"""
pass
# === 位置渲染系统 ===
def render_position_widget(self, position, context, **kwargs):
"""
根据位置渲染插件组件
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
dict: {'html': 'HTML内容', 'priority': 优先级} None
"""
if position not in self.SUPPORTED_POSITIONS:
return None
# 检查条件显示
if not self.should_display(position, context, **kwargs):
return None
# 调用具体的位置渲染方法
method_name = f'render_{position}_widget'
if hasattr(self, method_name):
html = getattr(self, method_name)(context, **kwargs)
if html:
priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
return {
'html': html,
'priority': priority,
'plugin_name': self.PLUGIN_NAME
}
return None
def should_display(self, position, context, **kwargs):
"""
判断插件是否应该在指定位置显示
子类可重写此方法实现条件显示逻辑
Args:
position: 位置标识
context: 模板上下文
**kwargs: 额外参数
Returns:
bool: 是否显示
"""
return True
# === 各位置渲染方法 - 子类重写 ===
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏组件"""
return None
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部组件"""
return None
def render_article_top_widget(self, context, **kwargs):
"""渲染文章顶部组件"""
return None
def render_header_widget(self, context, **kwargs):
"""渲染页头组件"""
return None
def render_footer_widget(self, context, **kwargs):
"""渲染页脚组件"""
return None
def render_comment_before_widget(self, context, **kwargs):
"""渲染评论前组件"""
return None
def render_comment_after_widget(self, context, **kwargs):
"""渲染评论后组件"""
return None
# === 模板系统 ===
def render_template(self, template_name, context=None):
"""
渲染插件模板
Args:
template_name: 模板文件名
context: 模板上下文
Returns:
HTML字符串
"""
if context is None:
context = {}
template_path = f"plugins/{self.plugin_slug}/{template_name}"
try:
return render_to_string(template_path, context)
except TemplateDoesNotExist:
logger.warning(f"Plugin template not found: {template_path}")
return ""
# === 静态资源系统 ===
def get_static_url(self, static_file):
"""获取插件静态文件URL"""
from django.templatetags.static import static
return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
def get_css_files(self):
"""获取插件CSS文件列表"""
return []
def get_js_files(self):
"""获取插件JavaScript文件列表"""
return []
def get_head_html(self, context=None):
"""获取需要插入到<head>中的HTML内容"""
return ""
def get_body_html(self, context=None):
"""获取需要插入到<body>底部的HTML内容"""
return ""
def get_plugin_info(self):
"""
获取插件信息
@ -37,5 +185,10 @@ class BasePlugin:
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
'version': self.PLUGIN_VERSION,
'author': self.PLUGIN_AUTHOR,
'slug': self.plugin_slug,
'directory': str(self.plugin_dir),
'supported_positions': self.SUPPORTED_POSITIONS,
'priorities': self.POSITION_PRIORITIES
}

@ -5,3 +5,18 @@ ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 位置钩子常量
POSITION_HOOKS = {
'article_top': 'article_top_widgets',
'article_bottom': 'article_bottom_widgets',
'sidebar': 'sidebar_widgets',
'header': 'header_widgets',
'footer': 'footer_widgets',
'comment_before': 'comment_before_widgets',
'comment_after': 'comment_after_widgets',
}
# 资源注入钩子
HEAD_RESOURCES_HOOK = 'head_resources'
BODY_RESOURCES_HOOK = 'body_resources'

@ -4,16 +4,61 @@ from django.conf import settings
logger = logging.getLogger(__name__)
# 全局插件注册表
_loaded_plugins = []
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
global _loaded_plugins
_loaded_plugins = []
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
# 导入插件模块
plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
# 获取插件实例
if hasattr(plugin_module, 'plugin'):
plugin_instance = plugin_module.plugin
_loaded_plugins.append(plugin_instance)
logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
else:
logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
except AttributeError as e:
logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
except Exception as e:
logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
def get_loaded_plugins():
"""获取所有已加载的插件"""
return _loaded_plugins
def get_plugin_by_name(plugin_name):
"""根据名称获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_name:
return plugin
return None
def get_plugin_by_slug(plugin_slug):
"""根据slug获取插件"""
for plugin in _loaded_plugins:
if plugin.plugin_slug == plugin_slug:
return plugin
return None
def get_plugins_info():
"""获取所有插件的信息"""
return [plugin.get_plugin_info() for plugin in _loaded_plugins]
def get_plugins_by_position(position):
"""获取支持指定位置的插件"""
return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]

@ -177,6 +177,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
# 添加插件静态文件目录
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'plugins'), # 让Django能找到插件的静态文件
]
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
@ -301,19 +306,57 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 根据环境变量决定是否启用离线压缩
COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
# 压缩输出目录
COMPRESS_OUTPUT_DIR = 'compressed'
# 压缩文件名模板 - 包含哈希值用于缓存破坏
COMPRESS_CSS_HASHING_METHOD = 'mtime'
COMPRESS_JS_HASHING_METHOD = 'mtime'
# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
# 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
# CSS压缩器 - 高压缩等级
'compressor.filters.cssmin.CSSCompressorFilter',
]
# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
# JS压缩器 - 高压缩等级
'compressor.filters.jsmin.SlimItFilter',
]
# 压缩缓存配置
COMPRESS_CACHE_BACKEND = 'default'
COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
# 预压缩配置
COMPRESS_PRECOMPILERS = (
# 支持SCSS/SASS
('text/x-scss', 'django_libsass.SassCompiler'),
('text/x-sass', 'django_libsass.SassCompiler'),
)
# 压缩性能优化
COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时30天
# 压缩等级配置
COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
# 静态文件缓存配置
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# 浏览器缓存配置(通过中间件或服务器配置)
COMPRESS_URL = STATIC_URL
COMPRESS_ROOT = STATIC_ROOT
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
@ -356,5 +399,6 @@ ACTIVE_PLUGINS = [
'view_count',
'seo_optimizer',
'image_lazy_loading',
'article_recommendation',
]

@ -0,0 +1,205 @@
import logging
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
from blog.models import Article
logger = logging.getLogger(__name__)
class ArticleRecommendationPlugin(BasePlugin):
PLUGIN_NAME = '文章推荐'
PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示'
PLUGIN_VERSION = '1.0.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 支持的位置
SUPPORTED_POSITIONS = ['article_bottom']
# 各位置优先级
POSITION_PRIORITIES = {
'article_bottom': 80, # 文章底部优先级
}
# 插件配置
CONFIG = {
'article_bottom_count': 8, # 文章底部推荐数量
'sidebar_count': 5, # 侧边栏推荐数量
'enable_category_fallback': True, # 启用分类回退
'enable_popular_fallback': True, # 启用热门文章回退
}
def register_hooks(self):
"""注册钩子"""
hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
def on_article_detail_load(self, article, context, request, *args, **kwargs):
"""文章详情页加载时的处理"""
# 可以在这里预加载推荐数据到context中
recommendations = self.get_recommendations(article)
context['article_recommendations'] = recommendations
def should_display(self, position, context, **kwargs):
"""条件显示逻辑"""
# 只在文章详情页底部显示
if position == 'article_bottom':
article = kwargs.get('article') or context.get('article')
# 检查是否有文章对象,以及是否不是索引页面
is_index = context.get('isindex', False) if hasattr(context, 'get') else False
return article is not None and not is_index
return False
def render_article_bottom_widget(self, context, **kwargs):
"""渲染文章底部推荐"""
article = kwargs.get('article') or context.get('article')
if not article:
return None
# 使用配置的数量也可以通过kwargs覆盖
count = kwargs.get('count', self.CONFIG['article_bottom_count'])
recommendations = self.get_recommendations(article, count=count)
if not recommendations:
return None
# 将RequestContext转换为普通字典
context_dict = {}
if hasattr(context, 'flatten'):
context_dict = context.flatten()
elif hasattr(context, 'dicts'):
# 合并所有上下文字典
for d in context.dicts:
context_dict.update(d)
template_context = {
'recommendations': recommendations,
'article': article,
'title': '相关推荐',
**context_dict
}
return self.render_template('bottom_widget.html', template_context)
def render_sidebar_widget(self, context, **kwargs):
"""渲染侧边栏推荐"""
article = context.get('article')
# 使用配置的数量也可以通过kwargs覆盖
count = kwargs.get('count', self.CONFIG['sidebar_count'])
if article:
# 文章页面,显示相关文章
recommendations = self.get_recommendations(article, count=count)
title = '相关文章'
else:
# 其他页面,显示热门文章
recommendations = self.get_popular_articles(count=count)
title = '热门推荐'
if not recommendations:
return None
# 将RequestContext转换为普通字典
context_dict = {}
if hasattr(context, 'flatten'):
context_dict = context.flatten()
elif hasattr(context, 'dicts'):
# 合并所有上下文字典
for d in context.dicts:
context_dict.update(d)
template_context = {
'recommendations': recommendations,
'title': title,
**context_dict
}
return self.render_template('sidebar_widget.html', template_context)
def get_css_files(self):
"""返回CSS文件"""
return ['css/recommendation.css']
def get_js_files(self):
"""返回JS文件"""
return ['js/recommendation.js']
def get_recommendations(self, article, count=5):
"""获取推荐文章"""
if not article:
return []
recommendations = []
# 1. 基于标签的推荐
if article.tags.exists():
tag_ids = list(article.tags.values_list('id', flat=True))
tag_based = list(Article.objects.filter(
status='p',
tags__id__in=tag_ids
).exclude(
id=article.id
).exclude(
title__isnull=True
).exclude(
title__exact=''
).distinct().order_by('-views')[:count])
recommendations.extend(tag_based)
# 2. 如果数量不够,基于分类推荐
if len(recommendations) < count and self.CONFIG['enable_category_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
category_based = list(Article.objects.filter(
status='p',
category=article.category
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed])
recommendations.extend(category_based)
# 3. 如果还是不够,推荐热门文章
if len(recommendations) < count and self.CONFIG['enable_popular_fallback']:
needed = count - len(recommendations)
existing_ids = [r.id for r in recommendations] + [article.id]
popular_articles = list(Article.objects.filter(
status='p'
).exclude(
id__in=existing_ids
).exclude(
title__isnull=True
).exclude(
title__exact=''
).order_by('-views')[:needed])
recommendations.extend(popular_articles)
# 过滤掉无效的推荐
valid_recommendations = []
for rec in recommendations:
if rec.title and len(rec.title.strip()) > 0:
valid_recommendations.append(rec)
else:
logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'")
# 调试:记录推荐结果
logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}")
for i, rec in enumerate(valid_recommendations):
logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}")
return valid_recommendations[:count]
def get_popular_articles(self, count=3):
"""获取热门文章"""
return list(Article.objects.filter(
status='p'
).order_by('-views')[:count])
# 实例化插件
plugin = ArticleRecommendationPlugin()

@ -0,0 +1,166 @@
/* 文章推荐插件样式 - 与网站风格保持一致 */
/* 文章底部推荐样式 */
.article-recommendations {
margin: 30px 0;
padding: 20px;
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.recommendations-title {
margin: 0 0 15px 0;
font-size: 18px;
color: #444;
font-weight: bold;
padding-bottom: 8px;
border-bottom: 2px solid #21759b;
display: inline-block;
}
.recommendations-icon {
margin-right: 5px;
font-size: 16px;
}
.recommendations-grid {
display: grid;
gap: 15px;
grid-template-columns: 1fr;
margin-top: 15px;
}
.recommendation-card {
background: #fff;
border: 1px solid #e1e1e1;
border-radius: 3px;
transition: all 0.2s ease;
overflow: hidden;
}
.recommendation-card:hover {
border-color: #21759b;
box-shadow: 0 2px 5px rgba(33, 117, 155, 0.1);
}
.recommendation-link {
display: block;
padding: 15px;
text-decoration: none;
color: inherit;
}
.recommendation-title {
margin: 0 0 8px 0;
font-size: 15px;
font-weight: normal;
color: #444;
line-height: 1.4;
transition: color 0.2s ease;
}
.recommendation-card:hover .recommendation-title {
color: #21759b;
}
.recommendation-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #757575;
}
.recommendation-category {
background: #ebebeb;
color: #5e5e5e;
padding: 2px 6px;
border-radius: 2px;
font-size: 11px;
font-weight: normal;
}
.recommendation-date {
font-weight: normal;
color: #757575;
}
/* 侧边栏推荐样式 */
.widget_recommendations {
margin-bottom: 20px;
}
.widget_recommendations .widget-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #007cba;
padding-bottom: 5px;
}
.recommendations-list {
list-style: none;
padding: 0;
margin: 0;
}
.recommendations-list .recommendation-item {
padding: 8px 0;
border-bottom: 1px solid #eee;
background: none;
border: none;
border-radius: 0;
}
.recommendations-list .recommendation-item:last-child {
border-bottom: none;
}
.recommendations-list .recommendation-item a {
color: #333;
text-decoration: none;
font-size: 14px;
line-height: 1.4;
display: block;
margin-bottom: 4px;
transition: color 0.3s ease;
}
.recommendations-list .recommendation-item a:hover {
color: #007cba;
}
.recommendations-list .recommendation-meta {
font-size: 11px;
color: #999;
margin: 0;
}
.recommendations-list .recommendation-meta span {
margin-right: 10px;
}
/* 响应式设计 - 分栏显示 */
@media (min-width: 768px) {
.recommendations-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
}
@media (min-width: 1024px) {
.recommendations-grid {
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
}
@media (min-width: 1200px) {
.recommendations-grid {
grid-template-columns: repeat(4, 1fr);
gap: 15px;
}
}

@ -0,0 +1,93 @@
/**
* 文章推荐插件JavaScript
*/
(function() {
'use strict';
// 等待DOM加载完成
document.addEventListener('DOMContentLoaded', function() {
initRecommendations();
});
function initRecommendations() {
// 添加点击统计
trackRecommendationClicks();
// 懒加载优化(如果需要)
lazyLoadRecommendations();
}
function trackRecommendationClicks() {
const recommendationLinks = document.querySelectorAll('.recommendation-item a');
recommendationLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
// 可以在这里添加点击统计逻辑
const articleTitle = this.textContent.trim();
const articleUrl = this.href;
// 发送统计数据到后端(可选)
if (typeof gtag !== 'undefined') {
gtag('event', 'click', {
'event_category': 'recommendation',
'event_label': articleTitle,
'value': 1
});
}
console.log('Recommendation clicked:', articleTitle, articleUrl);
});
});
}
function lazyLoadRecommendations() {
// 如果推荐内容很多,可以实现懒加载
const recommendationContainer = document.querySelector('.article-recommendations');
if (!recommendationContainer) {
return;
}
// 检查是否在视窗中
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('loaded');
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1
});
const recommendationItems = document.querySelectorAll('.recommendation-item');
recommendationItems.forEach(function(item) {
observer.observe(item);
});
}
// 添加一些动画效果
function addAnimations() {
const recommendationItems = document.querySelectorAll('.recommendation-item');
recommendationItems.forEach(function(item, index) {
item.style.opacity = '0';
item.style.transform = 'translateY(20px)';
item.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
setTimeout(function() {
item.style.opacity = '1';
item.style.transform = 'translateY(0)';
}, index * 100);
});
}
// 如果需要,可以在这里添加更多功能
window.ArticleRecommendation = {
init: initRecommendations,
track: trackRecommendationClicks,
animate: addAnimations
};
})();

@ -71,4 +71,9 @@
{% load_article_metas article user %}
</article><!-- #post -->
</article><!-- #post -->
<!-- 文章底部插件 -->
{% if not isindex %}
{% render_plugin_widgets 'article_bottom' article=article %}
{% endif %}

@ -5,9 +5,6 @@
<footer class="entry-meta">
{% trans 'posted in' %}
<a href="{{ article.category.get_absolute_url }}" rel="category tag">{{ article.category.name }}</a>
</a>
{% if article.type == 'a' %}
{% if article.tags.all %}
@ -46,13 +43,14 @@
title="{% datetimeformat article.pub_time %}"
itemprop="datePublished" content="{% datetimeformat article.pub_time %}"
rel="bookmark">
<time class="entry-date updated"
datetime="{{ article.pub_time }}">
{% datetimeformat article.pub_time %}</time>
{% if user.is_superuser %}
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a>
{% endif %}
<time class="entry-date updated"
datetime="{{ article.pub_time }}">
{% datetimeformat article.pub_time %}
</time>
</a>
{% if user.is_superuser %}
<a href="{{ article.get_admin_url }}">{% trans 'edit' %}</a>
{% endif %}
</span>
</footer><!-- .entry-meta -->

@ -0,0 +1,23 @@
{% load i18n %}
<div class="article-recommendations">
<h3 class="recommendations-title">
<span class="recommendations-icon">📖</span>{{ title }}
</h3>
<div class="recommendations-grid">
{% for article in recommendations %}
{% if article.title and article.title|length > 0 %}
<div class="recommendation-card">
<a href="{{ article.get_absolute_url }}" class="recommendation-link" title="{{ article.title }}">
<div class="recommendation-title">{{ article.title|truncatechars:45 }}</div>
<div class="recommendation-meta">
{% if article.category %}
<span class="recommendation-category">{{ article.category.name }}</span>
{% endif %}
<span class="recommendation-date">{{ article.pub_time|date:"m-d" }}</span>
</div>
</a>
</div>
{% endif %}
{% endfor %}
</div>
</div>

@ -0,0 +1,17 @@
{% load i18n %}
<aside class="widget widget_recommendations">
<p class="widget-title">{{ title }}</p>
<ul class="recommendations-list">
{% for article in recommendations %}
<li class="recommendation-item">
<a href="{{ article.get_absolute_url }}" title="{{ article.title }}">
{{ article.title|truncatechars:35 }}
</a>
<div class="recommendation-meta">
<span class="recommendation-views">{{ article.views }} {% trans 'views' %}</span>
<span class="recommendation-date">{{ article.pub_time|date:"m-d" }}</span>
</div>
</li>
{% endfor %}
</ul>
</aside>

@ -0,0 +1,4 @@
{% comment %}插件CSS文件包含模板 - 用于压缩{% endcomment %}
{% for css_file in css_files %}
<link rel="stylesheet" href="{{ css_file }}" type="text/css">
{% endfor %}

@ -0,0 +1,4 @@
{% comment %}插件JS文件包含模板 - 用于压缩{% endcomment %}
{% for js_file in js_files %}
<script src="{{ js_file }}"></script>
{% endfor %}

@ -25,12 +25,13 @@
<!-- SEO插件会自动生成title、description、keywords等标签 -->
{% endblock %}
<link rel="profile" href="http://gmpg.org/xfn/11"/>
<!-- DNS预解析 -->
<!-- 资源提示和预加载优化 -->
<link rel="dns-prefetch" href="//cdn.mathjax.org"/>
<link rel="dns-prefetch" href="//cdn.jsdelivr.net"/>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin/>
<!--[if lt IE 9]>
<script src="{% static 'blog/js/html5.js' %}" type="text/javascript"></script>
<![endif]-->
@ -40,11 +41,10 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="icon" href="/favicon.ico" type="image/x-icon"/>
<link rel="apple-touch-icon" href="/favicon.ico"/>
<!-- 本地字体加载 -->
<link rel="stylesheet" href="{% static 'blog/fonts/open-sans.css' %}">
{% compress css %}
<!-- 本地字体加载 -->
<link rel="stylesheet" href="{% static 'blog/fonts/open-sans.css' %}">
{% compress css %}
<link rel='stylesheet' id='twentytwelve-style-css' href='{% static 'blog/css/style.css' %}' type='text/css'
media='all'/>
<link href="{% static 'blog/css/oauth_style.css' %}" rel="stylesheet">
@ -56,11 +56,16 @@
<link rel="stylesheet" href="{% static 'blog/css/nprogress.css' %}">
{% block compress_css %}
{% endblock %}
<!-- 插件CSS文件 - 集成到压缩系统 -->
{% plugin_compressed_css %}
{% endcompress %}
{% if GLOBAL_HEADER %}
{{ GLOBAL_HEADER|safe }}
{% endif %}
<!-- 插件head资源 -->
{% plugin_head_resources %}
</head>
<body class="home blog custom-font-enabled">
@ -90,20 +95,25 @@
{% include 'share_layout/footer.html' %}
</div><!-- #page -->
<!-- JavaScript资源 -->
{% compress js %}
<script src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script>
{% block compress_js %}
{% endblock %}
{% endcompress %}
<!-- MathJax智能加载器 -->
<script src="{% static 'blog/js/mathjax-loader.js' %}" async defer></script>
{% block footer %}
<!-- JavaScript资源 -->
{% compress js %}
<script src="{% static 'blog/js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'blog/js/nprogress.js' %}"></script>
<script src="{% static 'blog/js/blog.js' %}"></script>
<script src="{% static 'blog/js/navigation.js' %}"></script>
{% block compress_js %}
{% endblock %}
<!-- 插件JS文件 - 集成到压缩系统 -->
{% plugin_compressed_js %}
{% endcompress %}
<!-- MathJax智能加载器 -->
<script src="{% static 'blog/js/mathjax-loader.js' %}" async defer></script>
{% block footer %}
{% endblock %}
<!-- 插件body资源 -->
{% plugin_body_resources %}
</body>
</html>

Loading…
Cancel
Save