@ -0,0 +1,110 @@
|
||||
# 文章草稿 Admin 管理界面
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse
|
||||
|
||||
from blog.models_draft import ArticleDraft
|
||||
|
||||
|
||||
@admin.register(ArticleDraft)
|
||||
class ArticleDraftAdmin(admin.ModelAdmin):
|
||||
"""文章草稿管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'title_display',
|
||||
'author',
|
||||
'article_link',
|
||||
'preview',
|
||||
'last_update_time',
|
||||
'is_published',
|
||||
'action_buttons'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'is_published',
|
||||
'author',
|
||||
'last_update_time',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'title',
|
||||
'body',
|
||||
'author__username'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'author',
|
||||
'article',
|
||||
'creation_time',
|
||||
'last_update_time',
|
||||
'session_id'
|
||||
]
|
||||
|
||||
fields = [
|
||||
'author',
|
||||
'article',
|
||||
'title',
|
||||
'body',
|
||||
'category_id',
|
||||
'tags_data',
|
||||
'status',
|
||||
'comment_status',
|
||||
'type',
|
||||
'session_id',
|
||||
'is_published',
|
||||
'creation_time',
|
||||
'last_update_time'
|
||||
]
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""禁止手动添加草稿"""
|
||||
return False
|
||||
|
||||
def title_display(self, obj):
|
||||
"""显示标题"""
|
||||
title = obj.title or '(无标题)'
|
||||
if len(title) > 40:
|
||||
return title[:37] + '...'
|
||||
return title
|
||||
|
||||
title_display.short_description = '标题'
|
||||
|
||||
def article_link(self, obj):
|
||||
"""文章链接"""
|
||||
if obj.article:
|
||||
url = reverse('admin:blog_article_change', args=[obj.article.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.article.title)
|
||||
return '-'
|
||||
|
||||
article_link.short_description = '文章'
|
||||
|
||||
def preview(self, obj):
|
||||
"""预览文本"""
|
||||
preview_text = obj.get_preview_text(30)
|
||||
return format_html('<span style="color: #666;">{}</span>', preview_text)
|
||||
|
||||
preview.short_description = '预览'
|
||||
|
||||
def action_buttons(self, obj):
|
||||
"""操作按钮"""
|
||||
if not obj.is_published:
|
||||
apply_url = f'/blog/api/draft/apply/'
|
||||
delete_url = f'/blog/api/draft/delete/'
|
||||
|
||||
return format_html(
|
||||
'<button class="button" onclick="applyDraft({})">应用到文章</button> '
|
||||
'<button class="button" onclick="deleteDraft({})">删除</button>',
|
||||
obj.id, obj.id
|
||||
)
|
||||
return '已发布'
|
||||
|
||||
action_buttons.short_description = '操作'
|
||||
|
||||
class Media:
|
||||
js = ('admin/js/draft_actions.js',)
|
||||
@ -0,0 +1,210 @@
|
||||
# 多媒体管理 Admin 界面
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse
|
||||
|
||||
from blog.models_media import MediaFile, MediaFolder, MediaFileFolder
|
||||
|
||||
|
||||
@admin.register(MediaFile)
|
||||
class MediaFileAdmin(admin.ModelAdmin):
|
||||
"""媒体文件管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'thumbnail_preview',
|
||||
'original_filename',
|
||||
'file_type',
|
||||
'file_size_display',
|
||||
'uploader_link',
|
||||
'upload_time',
|
||||
'reference_count',
|
||||
'is_public'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'file_type',
|
||||
'is_public',
|
||||
'upload_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'original_filename',
|
||||
'description',
|
||||
'uploader__username'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'stored_filename',
|
||||
'file_hash',
|
||||
'file_size',
|
||||
'mime_type',
|
||||
'file_path',
|
||||
'thumbnail_path',
|
||||
'width',
|
||||
'height',
|
||||
'upload_time',
|
||||
'thumbnail_preview_large'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('original_filename', 'file_type', 'description', 'is_public')
|
||||
}),
|
||||
('文件详情', {
|
||||
'fields': ('stored_filename', 'file_size', 'file_hash', 'mime_type', 'file_path')
|
||||
}),
|
||||
('图片信息', {
|
||||
'fields': ('width', 'height', 'thumbnail_path', 'thumbnail_preview_large'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('用户信息', {
|
||||
'fields': ('uploader', 'upload_time', 'reference_count')
|
||||
}),
|
||||
)
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
def thumbnail_preview(self, obj):
|
||||
"""缩略图预览(列表)"""
|
||||
if obj.is_image():
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 50px; max-height: 50px; object-fit: cover;" />',
|
||||
obj.get_thumbnail_url()
|
||||
)
|
||||
return '📄'
|
||||
|
||||
thumbnail_preview.short_description = '预览'
|
||||
|
||||
def thumbnail_preview_large(self, obj):
|
||||
"""缩略图预览(详情)"""
|
||||
if obj.is_image():
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 300px; max-height: 300px;" />',
|
||||
obj.get_absolute_url()
|
||||
)
|
||||
return '非图片文件'
|
||||
|
||||
thumbnail_preview_large.short_description = '图片预览'
|
||||
|
||||
def file_size_display(self, obj):
|
||||
"""文件大小显示"""
|
||||
size = obj.file_size
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.1f} {unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.1f} TB"
|
||||
|
||||
file_size_display.short_description = '文件大小'
|
||||
|
||||
def uploader_link(self, obj):
|
||||
"""上传者链接"""
|
||||
if obj.uploader:
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.uploader.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.uploader.username)
|
||||
return '-'
|
||||
|
||||
uploader_link.short_description = '上传者'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""禁止手动添加(应通过上传功能)"""
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(MediaFolder)
|
||||
class MediaFolderAdmin(admin.ModelAdmin):
|
||||
"""媒体文件夹管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'name',
|
||||
'full_path_display',
|
||||
'owner_link',
|
||||
'files_count_display',
|
||||
'created_time'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'created_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
'description',
|
||||
'owner__username'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'created_time',
|
||||
'full_path_display'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('文件夹信息', {
|
||||
'fields': ('name', 'parent', 'owner', 'description')
|
||||
}),
|
||||
('详细信息', {
|
||||
'fields': ('created_time', 'full_path_display')
|
||||
}),
|
||||
)
|
||||
|
||||
def full_path_display(self, obj):
|
||||
"""完整路径显示"""
|
||||
return obj.get_full_path()
|
||||
|
||||
full_path_display.short_description = '完整路径'
|
||||
|
||||
def owner_link(self, obj):
|
||||
"""所有者链接"""
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.owner.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.owner.username)
|
||||
|
||||
owner_link.short_description = '所有者'
|
||||
|
||||
def files_count_display(self, obj):
|
||||
"""文件数量显示"""
|
||||
return obj.file_relations.count()
|
||||
|
||||
files_count_display.short_description = '文件数量'
|
||||
|
||||
|
||||
@admin.register(MediaFileFolder)
|
||||
class MediaFileFolderAdmin(admin.ModelAdmin):
|
||||
"""媒体文件-文件夹关联管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'file_link',
|
||||
'folder_link',
|
||||
'added_time'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'added_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'file__original_filename',
|
||||
'folder__name'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'added_time'
|
||||
]
|
||||
|
||||
def file_link(self, obj):
|
||||
"""文件链接"""
|
||||
url = reverse('admin:blog_mediafile_change', args=[obj.file.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.file.original_filename)
|
||||
|
||||
file_link.short_description = '文件'
|
||||
|
||||
def folder_link(self, obj):
|
||||
"""文件夹链接"""
|
||||
url = reverse('admin:blog_mediafolder_change', args=[obj.folder.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.folder.name)
|
||||
|
||||
folder_link.short_description = '文件夹'
|
||||
@ -0,0 +1,168 @@
|
||||
# 用户关注和收藏 Admin 管理界面
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import reverse
|
||||
|
||||
from blog.models_social import UserFollow, ArticleFavorite, ArticleLike
|
||||
|
||||
|
||||
@admin.register(UserFollow)
|
||||
class UserFollowAdmin(admin.ModelAdmin):
|
||||
"""用户关注管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'follower_link',
|
||||
'following_link',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'follower__username',
|
||||
'following__username'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'follower',
|
||||
'following',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""禁止手动添加"""
|
||||
return False
|
||||
|
||||
def follower_link(self, obj):
|
||||
"""关注者链接"""
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.follower.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.follower.username)
|
||||
|
||||
follower_link.short_description = '关注者'
|
||||
|
||||
def following_link(self, obj):
|
||||
"""被关注者链接"""
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.following.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.following.username)
|
||||
|
||||
following_link.short_description = '被关注者'
|
||||
|
||||
|
||||
@admin.register(ArticleFavorite)
|
||||
class ArticleFavoriteAdmin(admin.ModelAdmin):
|
||||
"""文章收藏管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'user_link',
|
||||
'article_link',
|
||||
'note_display',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'user__username',
|
||||
'article__title',
|
||||
'note'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'user',
|
||||
'article',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""禁止手动添加"""
|
||||
return False
|
||||
|
||||
def user_link(self, obj):
|
||||
"""用户链接"""
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.user.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
||||
|
||||
user_link.short_description = '用户'
|
||||
|
||||
def article_link(self, obj):
|
||||
"""文章链接"""
|
||||
url = reverse('admin:blog_article_change', args=[obj.article.id])
|
||||
title = obj.article.title
|
||||
if len(title) > 50:
|
||||
title = title[:47] + '...'
|
||||
return format_html('<a href="{}">{}</a>', url, title)
|
||||
|
||||
article_link.short_description = '文章'
|
||||
|
||||
def note_display(self, obj):
|
||||
"""显示备注"""
|
||||
if obj.note:
|
||||
if len(obj.note) > 30:
|
||||
return obj.note[:27] + '...'
|
||||
return obj.note
|
||||
return '-'
|
||||
|
||||
note_display.short_description = '备注'
|
||||
|
||||
|
||||
@admin.register(ArticleLike)
|
||||
class ArticleLikeAdmin(admin.ModelAdmin):
|
||||
"""文章点赞管理界面"""
|
||||
|
||||
list_display = [
|
||||
'id',
|
||||
'user_link',
|
||||
'article_link',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'user__username',
|
||||
'article__title'
|
||||
]
|
||||
|
||||
readonly_fields = [
|
||||
'user',
|
||||
'article',
|
||||
'creation_time'
|
||||
]
|
||||
|
||||
list_per_page = 50
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""禁止手动添加"""
|
||||
return False
|
||||
|
||||
def user_link(self, obj):
|
||||
"""用户链接"""
|
||||
url = reverse('admin:accounts_bloguser_change', args=[obj.user.id])
|
||||
return format_html('<a href="{}">{}</a>', url, obj.user.username)
|
||||
|
||||
user_link.short_description = '用户'
|
||||
|
||||
def article_link(self, obj):
|
||||
"""文章链接"""
|
||||
url = reverse('admin:blog_article_change', args=[obj.article.id])
|
||||
title = obj.article.title
|
||||
if len(title) > 50:
|
||||
title = title[:47] + '...'
|
||||
return format_html('<a href="{}">{}</a>', url, title)
|
||||
|
||||
article_link.short_description = '文章'
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
# 文章版本管理功能 - 数据库迁移
|
||||
# Generated manually for article version management feature
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0002_add_performance_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ArticleVersion',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('version_number', models.PositiveIntegerField(default=1, verbose_name='version number')),
|
||||
('title', models.CharField(max_length=200, verbose_name='title')),
|
||||
('body', models.TextField(verbose_name='body')),
|
||||
('pub_time', models.DateTimeField(verbose_name='publish time')),
|
||||
('status', models.CharField(max_length=1, verbose_name='status')),
|
||||
('comment_status', models.CharField(max_length=1, verbose_name='comment status')),
|
||||
('type', models.CharField(max_length=1, verbose_name='type')),
|
||||
('category_id', models.IntegerField(verbose_name='category id')),
|
||||
('category_name', models.CharField(max_length=30, verbose_name='category name')),
|
||||
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('change_summary', models.CharField(blank=True, default='', max_length=200, verbose_name='change summary')),
|
||||
('is_auto_save', models.BooleanField(default=True, verbose_name='is auto save')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='blog.Article', verbose_name='article')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_versions_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'article version',
|
||||
'verbose_name_plural': 'article versions',
|
||||
'ordering': ['-version_number'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articleversion',
|
||||
index=models.Index(fields=['article', '-version_number'], name='version_article_num_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articleversion',
|
||||
index=models.Index(fields=['creation_time'], name='version_creation_time_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='articleversion',
|
||||
unique_together={('article', 'version_number')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,54 @@
|
||||
# 文章草稿功能 - 数据库迁移
|
||||
# Generated manually for article draft auto-save feature
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0003_add_article_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ArticleDraft',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(blank=True, default='', max_length=200, verbose_name='title')),
|
||||
('body', models.TextField(blank=True, default='', verbose_name='body')),
|
||||
('category_id', models.IntegerField(blank=True, null=True, verbose_name='category id')),
|
||||
('tags_data', models.JSONField(blank=True, default=list, verbose_name='tags data')),
|
||||
('status', models.CharField(default='d', max_length=1, verbose_name='status')),
|
||||
('comment_status', models.CharField(default='o', max_length=1, verbose_name='comment status')),
|
||||
('type', models.CharField(default='a', max_length=1, verbose_name='type')),
|
||||
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('last_update_time', models.DateTimeField(auto_now=True, db_index=True, verbose_name='last update time')),
|
||||
('session_id', models.CharField(blank=True, default='', max_length=64, verbose_name='session id')),
|
||||
('is_published', models.BooleanField(default=False, verbose_name='is published')),
|
||||
('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='drafts', to='blog.Article', verbose_name='article')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_drafts', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'article draft',
|
||||
'verbose_name_plural': 'article drafts',
|
||||
'ordering': ['-last_update_time'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articledraft',
|
||||
index=models.Index(fields=['author', '-last_update_time'], name='draft_author_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articledraft',
|
||||
index=models.Index(fields=['article', '-last_update_time'], name='draft_article_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articledraft',
|
||||
index=models.Index(fields=['is_published', '-last_update_time'], name='draft_published_time_idx'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,78 @@
|
||||
# 用户关注和收藏功能 - 数据库迁移
|
||||
# Generated manually for social features
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0004_add_article_draft'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 创建用户关注模型
|
||||
migrations.CreateModel(
|
||||
name='UserFollow',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('follower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='follower')),
|
||||
('following', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL, verbose_name='following')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user follow',
|
||||
'verbose_name_plural': 'user follows',
|
||||
'ordering': ['-creation_time'],
|
||||
},
|
||||
),
|
||||
|
||||
# 创建文章收藏模型
|
||||
migrations.CreateModel(
|
||||
name='ArticleFavorite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('note', models.CharField(blank=True, default='', max_length=200, verbose_name='note')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='blog.Article', verbose_name='article')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'article favorite',
|
||||
'verbose_name_plural': 'article favorites',
|
||||
'ordering': ['-creation_time'],
|
||||
},
|
||||
),
|
||||
|
||||
# 添加索引
|
||||
migrations.AddIndex(
|
||||
model_name='userfollow',
|
||||
index=models.Index(fields=['follower', '-creation_time'], name='follow_follower_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='userfollow',
|
||||
index=models.Index(fields=['following', '-creation_time'], name='follow_following_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articlefavorite',
|
||||
index=models.Index(fields=['user', '-creation_time'], name='favorite_user_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articlefavorite',
|
||||
index=models.Index(fields=['article', '-creation_time'], name='favorite_article_time_idx'),
|
||||
),
|
||||
|
||||
# 添加唯一约束
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userfollow',
|
||||
unique_together={('follower', 'following')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='articlefavorite',
|
||||
unique_together={('user', 'article')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,43 @@
|
||||
# Generated migration for ArticleLike model
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0005_add_social_features'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ArticleLike',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('creation_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='creation time')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_by', to='blog.article', verbose_name='article')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'article like',
|
||||
'verbose_name_plural': 'article likes',
|
||||
'ordering': ['-creation_time'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articlelike',
|
||||
index=models.Index(fields=['user', '-creation_time'], name='like_user_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='articlelike',
|
||||
index=models.Index(fields=['article', '-creation_time'], name='like_article_time_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='articlelike',
|
||||
unique_together={('user', 'article')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,101 @@
|
||||
# Generated migration for Media Management System
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0006_add_article_like'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 创建 MediaFile 模型
|
||||
migrations.CreateModel(
|
||||
name='MediaFile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('original_filename', models.CharField(max_length=255, verbose_name='original filename')),
|
||||
('stored_filename', models.CharField(max_length=255, unique=True, verbose_name='stored filename')),
|
||||
('file_type', models.CharField(choices=[('image', 'Image'), ('file', 'File')], max_length=10, verbose_name='file type')),
|
||||
('file_size', models.BigIntegerField(verbose_name='file size')),
|
||||
('file_hash', models.CharField(db_index=True, max_length=32, verbose_name='file hash')),
|
||||
('mime_type', models.CharField(max_length=100, verbose_name='MIME type')),
|
||||
('file_path', models.CharField(max_length=500, verbose_name='file path')),
|
||||
('upload_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='upload time')),
|
||||
('width', models.IntegerField(blank=True, null=True, verbose_name='width')),
|
||||
('height', models.IntegerField(blank=True, null=True, verbose_name='height')),
|
||||
('thumbnail_path', models.CharField(blank=True, max_length=500, verbose_name='thumbnail path')),
|
||||
('description', models.TextField(blank=True, verbose_name='description')),
|
||||
('is_public', models.BooleanField(default=True, verbose_name='is public')),
|
||||
('reference_count', models.IntegerField(default=0, verbose_name='reference count')),
|
||||
('uploader', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_files', to=settings.AUTH_USER_MODEL, verbose_name='uploader')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'media file',
|
||||
'verbose_name_plural': 'media files',
|
||||
'ordering': ['-upload_time'],
|
||||
},
|
||||
),
|
||||
|
||||
# 创建 MediaFolder 模型
|
||||
migrations.CreateModel(
|
||||
name='MediaFolder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='folder name')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created time')),
|
||||
('description', models.TextField(blank=True, verbose_name='description')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_folders', to=settings.AUTH_USER_MODEL, verbose_name='owner')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subfolders', to='blog.mediafolder', verbose_name='parent folder')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'media folder',
|
||||
'verbose_name_plural': 'media folders',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
|
||||
# 创建 MediaFileFolder 模型
|
||||
migrations.CreateModel(
|
||||
name='MediaFileFolder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('added_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='added time')),
|
||||
('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_relations', to='blog.mediafile')),
|
||||
('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='file_relations', to='blog.mediafolder')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'media file folder relation',
|
||||
'verbose_name_plural': 'media file folder relations',
|
||||
},
|
||||
),
|
||||
|
||||
# 添加索引
|
||||
migrations.AddIndex(
|
||||
model_name='mediafile',
|
||||
index=models.Index(fields=['file_type', '-upload_time'], name='media_type_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='mediafile',
|
||||
index=models.Index(fields=['uploader', '-upload_time'], name='media_uploader_time_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='mediafile',
|
||||
index=models.Index(fields=['file_hash'], name='media_hash_idx'),
|
||||
),
|
||||
|
||||
# 添加唯一约束
|
||||
migrations.AlterUniqueTogether(
|
||||
name='mediafolder',
|
||||
unique_together={('name', 'parent', 'owner')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='mediafilefolder',
|
||||
unique_together={('file', 'folder')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,14 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-24 02:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0006_alter_blogsettings_options'),
|
||||
('blog', '0007_add_media_management'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@ -0,0 +1,359 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-25 13:02
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0008_merge_20251124_0221'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='articledraft',
|
||||
options={'ordering': ['-last_update_time'], 'verbose_name': '文章草稿', 'verbose_name_plural': '文章草稿'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='articlefavorite',
|
||||
options={'ordering': ['-creation_time'], 'verbose_name': '文章收藏', 'verbose_name_plural': '文章收藏'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='articlelike',
|
||||
options={'ordering': ['-creation_time'], 'verbose_name': '文章点赞', 'verbose_name_plural': '文章点赞'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='articleversion',
|
||||
options={'ordering': ['-version_number'], 'verbose_name': '文章版本', 'verbose_name_plural': '文章版本'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mediafile',
|
||||
options={'ordering': ['-upload_time'], 'verbose_name': '媒体文件', 'verbose_name_plural': '媒体文件'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mediafilefolder',
|
||||
options={'verbose_name': '文件-文件夹关联', 'verbose_name_plural': '文件-文件夹关联'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mediafolder',
|
||||
options={'ordering': ['name'], 'verbose_name': '媒体文件夹', 'verbose_name_plural': '媒体文件夹'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userfollow',
|
||||
options={'ordering': ['-creation_time'], 'verbose_name': '用户关注', 'verbose_name_plural': '用户关注'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='article',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='drafts', to='blog.article', verbose_name='文章'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='article_drafts', to=settings.AUTH_USER_MODEL, verbose_name='作者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='body',
|
||||
field=models.TextField(blank=True, default='', verbose_name='正文'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='category_id',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='分类ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='comment_status',
|
||||
field=models.CharField(default='o', max_length=1, verbose_name='评论状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='is_published',
|
||||
field=models.BooleanField(default=False, verbose_name='已发布'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='last_update_time',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='最后更新时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='session_id',
|
||||
field=models.CharField(blank=True, default='', max_length=64, verbose_name='会话ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='status',
|
||||
field=models.CharField(default='d', max_length=1, verbose_name='状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='tags_data',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='标签数据'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, default='', max_length=200, verbose_name='标题'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articledraft',
|
||||
name='type',
|
||||
field=models.CharField(default='a', max_length=1, verbose_name='类型'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlefavorite',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='blog.article', verbose_name='文章'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlefavorite',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlefavorite',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlefavorite',
|
||||
name='note',
|
||||
field=models.CharField(blank=True, default='', max_length=200, verbose_name='备注'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlefavorite',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlelike',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_by', to='blog.article', verbose_name='文章'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlelike',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articlelike',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL, verbose_name='用户'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='blog.article', verbose_name='文章'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='body',
|
||||
field=models.TextField(verbose_name='正文'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='category_id',
|
||||
field=models.IntegerField(verbose_name='分类ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='category_name',
|
||||
field=models.CharField(max_length=30, verbose_name='分类名称'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='change_summary',
|
||||
field=models.CharField(blank=True, default='', max_length=200, verbose_name='变更说明'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='comment_status',
|
||||
field=models.CharField(max_length=1, verbose_name='评论状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_versions_created', to=settings.AUTH_USER_MODEL, verbose_name='创建者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='is_auto_save',
|
||||
field=models.BooleanField(default=True, verbose_name='自动保存'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='pub_time',
|
||||
field=models.DateTimeField(verbose_name='发布时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='status',
|
||||
field=models.CharField(max_length=1, verbose_name='状态'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='title',
|
||||
field=models.CharField(max_length=200, verbose_name='标题'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='type',
|
||||
field=models.CharField(max_length=1, verbose_name='类型'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleversion',
|
||||
name='version_number',
|
||||
field=models.PositiveIntegerField(default=1, verbose_name='版本号'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, verbose_name='描述'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='file_hash',
|
||||
field=models.CharField(db_index=True, max_length=32, verbose_name='文件哈希'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='file_path',
|
||||
field=models.CharField(max_length=500, verbose_name='文件路径'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='file_size',
|
||||
field=models.BigIntegerField(verbose_name='文件大小'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='file_type',
|
||||
field=models.CharField(choices=[('image', '图片'), ('file', '文件')], max_length=10, verbose_name='文件类型'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='height',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='高度'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='is_public',
|
||||
field=models.BooleanField(default=True, verbose_name='是否公开'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='mime_type',
|
||||
field=models.CharField(max_length=100, verbose_name='MIME类型'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='original_filename',
|
||||
field=models.CharField(max_length=255, verbose_name='原始文件名'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='reference_count',
|
||||
field=models.IntegerField(default=0, verbose_name='引用次数'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='stored_filename',
|
||||
field=models.CharField(max_length=255, unique=True, verbose_name='存储文件名'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='thumbnail_path',
|
||||
field=models.CharField(blank=True, max_length=500, verbose_name='缩略图路径'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='upload_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='上传时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='uploader',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='uploaded_files', to=settings.AUTH_USER_MODEL, verbose_name='上传者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafile',
|
||||
name='width',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='宽度'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafilefolder',
|
||||
name='added_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='添加时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafolder',
|
||||
name='created_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafolder',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, verbose_name='描述'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafolder',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, verbose_name='文件夹名称'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafolder',
|
||||
name='owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_folders', to=settings.AUTH_USER_MODEL, verbose_name='所有者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mediafolder',
|
||||
name='parent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subfolders', to='blog.mediafolder', verbose_name='父文件夹'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollow',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='创建时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollow',
|
||||
name='follower',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='following', to=settings.AUTH_USER_MODEL, verbose_name='关注者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollow',
|
||||
name='following',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='followers', to=settings.AUTH_USER_MODEL, verbose_name='被关注者'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userfollow',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,401 @@
|
||||
# 用户关注和收藏功能模型
|
||||
# 实现用户间的关注关系和文章收藏功能
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from blog.models import Article
|
||||
|
||||
|
||||
class UserFollow(models.Model):
|
||||
"""
|
||||
用户关注模型
|
||||
记录用户之间的关注关系
|
||||
"""
|
||||
# 关注者(谁关注了别人)
|
||||
follower = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name='关注者',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='following'
|
||||
)
|
||||
|
||||
# 被关注者(被谁关注)
|
||||
following = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name='被关注者',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='followers'
|
||||
)
|
||||
|
||||
# 关注时间
|
||||
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-creation_time']
|
||||
verbose_name = '用户关注'
|
||||
verbose_name_plural = '用户关注'
|
||||
unique_together = [['follower', 'following']]
|
||||
indexes = [
|
||||
models.Index(fields=['follower', '-creation_time'], name='follow_follower_time_idx'),
|
||||
models.Index(fields=['following', '-creation_time'], name='follow_following_time_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.follower.username} -> {self.following.username}"
|
||||
|
||||
def clean(self):
|
||||
"""验证:不能关注自己"""
|
||||
from django.core.exceptions import ValidationError
|
||||
if self.follower == self.following:
|
||||
raise ValidationError(_('Cannot follow yourself'))
|
||||
|
||||
@classmethod
|
||||
def is_following(cls, follower, following):
|
||||
"""
|
||||
检查是否已关注
|
||||
|
||||
Args:
|
||||
follower: 关注者用户对象
|
||||
following: 被关注者用户对象
|
||||
|
||||
Returns:
|
||||
bool: 是否已关注
|
||||
"""
|
||||
return cls.objects.filter(follower=follower, following=following).exists()
|
||||
|
||||
@classmethod
|
||||
def follow(cls, follower, following):
|
||||
"""
|
||||
关注用户
|
||||
|
||||
Args:
|
||||
follower: 关注者用户对象
|
||||
following: 被关注者用户对象
|
||||
|
||||
Returns:
|
||||
UserFollow 实例或 None
|
||||
"""
|
||||
if follower == following:
|
||||
return None
|
||||
|
||||
follow, created = cls.objects.get_or_create(
|
||||
follower=follower,
|
||||
following=following
|
||||
)
|
||||
return follow if created else None
|
||||
|
||||
@classmethod
|
||||
def unfollow(cls, follower, following):
|
||||
"""
|
||||
取消关注
|
||||
|
||||
Args:
|
||||
follower: 关注者用户对象
|
||||
following: 被关注者用户对象
|
||||
|
||||
Returns:
|
||||
bool: 是否成功取消关注
|
||||
"""
|
||||
deleted_count, _ = cls.objects.filter(
|
||||
follower=follower,
|
||||
following=following
|
||||
).delete()
|
||||
return deleted_count > 0
|
||||
|
||||
@classmethod
|
||||
def get_following_list(cls, user, limit=None):
|
||||
"""
|
||||
获取用户关注的人列表
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
QuerySet: 关注的用户列表
|
||||
"""
|
||||
queryset = cls.objects.filter(follower=user).select_related('following')
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
return [f.following for f in queryset]
|
||||
|
||||
@classmethod
|
||||
def get_followers_list(cls, user, limit=None):
|
||||
"""
|
||||
获取用户的粉丝列表
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
QuerySet: 粉丝用户列表
|
||||
"""
|
||||
queryset = cls.objects.filter(following=user).select_related('follower')
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
return [f.follower for f in queryset]
|
||||
|
||||
@classmethod
|
||||
def get_following_count(cls, user):
|
||||
"""获取关注数量"""
|
||||
return cls.objects.filter(follower=user).count()
|
||||
|
||||
@classmethod
|
||||
def get_followers_count(cls, user):
|
||||
"""获取粉丝数量"""
|
||||
return cls.objects.filter(following=user).count()
|
||||
|
||||
|
||||
class ArticleFavorite(models.Model):
|
||||
"""
|
||||
文章收藏模型
|
||||
记录用户收藏的文章
|
||||
"""
|
||||
# 用户
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name='用户',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='favorites'
|
||||
)
|
||||
|
||||
# 文章
|
||||
article = models.ForeignKey(
|
||||
Article,
|
||||
verbose_name='文章',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='favorited_by'
|
||||
)
|
||||
|
||||
# 收藏时间
|
||||
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
|
||||
|
||||
# 收藏备注(可选)
|
||||
note = models.CharField('备注', max_length=200, blank=True, default='')
|
||||
|
||||
class Meta:
|
||||
ordering = ['-creation_time']
|
||||
verbose_name = '文章收藏'
|
||||
verbose_name_plural = '文章收藏'
|
||||
unique_together = [['user', 'article']]
|
||||
indexes = [
|
||||
models.Index(fields=['user', '-creation_time'], name='favorite_user_time_idx'),
|
||||
models.Index(fields=['article', '-creation_time'], name='favorite_article_time_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} -> {self.article.title}"
|
||||
|
||||
@classmethod
|
||||
def is_favorited(cls, user, article):
|
||||
"""
|
||||
检查是否已收藏
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
bool: 是否已收藏
|
||||
"""
|
||||
return cls.objects.filter(user=user, article=article).exists()
|
||||
|
||||
@classmethod
|
||||
def add_favorite(cls, user, article, note=''):
|
||||
"""
|
||||
收藏文章
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
note: 收藏备注
|
||||
|
||||
Returns:
|
||||
ArticleFavorite 实例或 None
|
||||
"""
|
||||
favorite, created = cls.objects.get_or_create(
|
||||
user=user,
|
||||
article=article,
|
||||
defaults={'note': note}
|
||||
)
|
||||
return favorite if created else None
|
||||
|
||||
@classmethod
|
||||
def remove_favorite(cls, user, article):
|
||||
"""
|
||||
取消收藏
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
bool: 是否成功取消收藏
|
||||
"""
|
||||
deleted_count, _ = cls.objects.filter(
|
||||
user=user,
|
||||
article=article
|
||||
).delete()
|
||||
return deleted_count > 0
|
||||
|
||||
@classmethod
|
||||
def get_user_favorites(cls, user, limit=None):
|
||||
"""
|
||||
获取用户的收藏列表
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
QuerySet: 收藏的文章列表
|
||||
"""
|
||||
queryset = cls.objects.filter(user=user).select_related('article', 'article__author', 'article__category')
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
return queryset
|
||||
|
||||
@classmethod
|
||||
def get_favorite_count(cls, user):
|
||||
"""获取用户收藏数量"""
|
||||
return cls.objects.filter(user=user).count()
|
||||
|
||||
@classmethod
|
||||
def get_article_favorite_count(cls, article):
|
||||
"""获取文章被收藏次数"""
|
||||
return cls.objects.filter(article=article).count()
|
||||
|
||||
|
||||
class ArticleLike(models.Model):
|
||||
"""
|
||||
文章点赞模型
|
||||
记录用户对文章的点赞
|
||||
"""
|
||||
# 用户
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name='用户',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='likes'
|
||||
)
|
||||
|
||||
# 文章
|
||||
article = models.ForeignKey(
|
||||
Article,
|
||||
verbose_name='文章',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='liked_by'
|
||||
)
|
||||
|
||||
# 点赞时间
|
||||
creation_time = models.DateTimeField('创建时间', default=now, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-creation_time']
|
||||
verbose_name = '文章点赞'
|
||||
verbose_name_plural = '文章点赞'
|
||||
unique_together = [['user', 'article']]
|
||||
indexes = [
|
||||
models.Index(fields=['user', '-creation_time'], name='like_user_time_idx'),
|
||||
models.Index(fields=['article', '-creation_time'], name='like_article_time_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} 👍 {self.article.title}"
|
||||
|
||||
@classmethod
|
||||
def is_liked(cls, user, article):
|
||||
"""
|
||||
检查是否已点赞
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
bool: 是否已点赞
|
||||
"""
|
||||
return cls.objects.filter(user=user, article=article).exists()
|
||||
|
||||
@classmethod
|
||||
def add_like(cls, user, article):
|
||||
"""
|
||||
点赞文章
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
ArticleLike 实例或 None
|
||||
"""
|
||||
like, created = cls.objects.get_or_create(
|
||||
user=user,
|
||||
article=article
|
||||
)
|
||||
return like if created else None
|
||||
|
||||
@classmethod
|
||||
def remove_like(cls, user, article):
|
||||
"""
|
||||
取消点赞
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
bool: 是否成功取消点赞
|
||||
"""
|
||||
deleted_count, _ = cls.objects.filter(
|
||||
user=user,
|
||||
article=article
|
||||
).delete()
|
||||
return deleted_count > 0
|
||||
|
||||
@classmethod
|
||||
def get_article_like_count(cls, article):
|
||||
"""
|
||||
获取文章点赞数
|
||||
|
||||
Args:
|
||||
article: 文章对象
|
||||
|
||||
Returns:
|
||||
int: 点赞数
|
||||
"""
|
||||
return cls.objects.filter(article=article).count()
|
||||
|
||||
@classmethod
|
||||
def get_user_like_count(cls, user):
|
||||
"""
|
||||
获取用户点赞数
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
|
||||
Returns:
|
||||
int: 用户点赞的文章数量
|
||||
"""
|
||||
return cls.objects.filter(user=user).count()
|
||||
|
||||
@classmethod
|
||||
def get_user_likes(cls, user, limit=None):
|
||||
"""
|
||||
获取用户点赞的文章列表
|
||||
|
||||
Args:
|
||||
user: 用户对象
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
QuerySet: 点赞的文章列表
|
||||
"""
|
||||
queryset = cls.objects.filter(user=user).select_related('article', 'article__author', 'article__category')
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
return queryset
|
||||
@ -0,0 +1,457 @@
|
||||
/* 深色模式修复 - 覆盖 style.css 中的硬编码白色背景 */
|
||||
|
||||
/* 覆盖所有白色背景为使用CSS变量 */
|
||||
[data-theme="dark"] .site-header,
|
||||
[data-theme="dark"] #masthead {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-content,
|
||||
[data-theme="dark"] #content {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget-area,
|
||||
[data-theme="dark"] #secondary {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-content,
|
||||
[data-theme="dark"] .entry-summary,
|
||||
[data-theme="dark"] .page-content,
|
||||
[data-theme="dark"] article {
|
||||
background-color: var(--article-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body {
|
||||
background-color: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有白色背景的元素 */
|
||||
[data-theme="dark"] *[style*="background: #fff"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff"],
|
||||
[data-theme="dark"] *[style*="background: white"],
|
||||
[data-theme="dark"] *[style*="background-color: white"] {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有白色文字的元素(排除按钮和链接) */
|
||||
[data-theme="dark"] *[style*="color: #fff"]:not(.btn):not(a),
|
||||
[data-theme="dark"] *[style*="color: white"]:not(.btn):not(a) {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 评论区修复 */
|
||||
[data-theme="dark"] #comments,
|
||||
[data-theme="dark"] .comment-list,
|
||||
[data-theme="dark"] .comment,
|
||||
[data-theme="dark"] .comment-body,
|
||||
[data-theme="dark"] .comment-content {
|
||||
background-color: var(--comment-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--comment-border) !important;
|
||||
}
|
||||
|
||||
/* 导航菜单修复 */
|
||||
[data-theme="dark"] .nav-menu,
|
||||
[data-theme="dark"] .main-navigation,
|
||||
[data-theme="dark"] #site-navigation {
|
||||
background-color: var(--nav-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu li,
|
||||
[data-theme="dark"] .main-navigation li {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu a,
|
||||
[data-theme="dark"] .main-navigation a {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu a:hover,
|
||||
[data-theme="dark"] .main-navigation a:hover {
|
||||
background-color: var(--nav-hover-bg) !important;
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* Widget 修复 */
|
||||
[data-theme="dark"] .widget {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--sidebar-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget-title {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget ul,
|
||||
[data-theme="dark"] .widget ol {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget a {
|
||||
color: var(--link-color) !important;
|
||||
}
|
||||
|
||||
/* 文章列表修复 */
|
||||
[data-theme="dark"] .hentry,
|
||||
[data-theme="dark"] .post,
|
||||
[data-theme="dark"] .page {
|
||||
background-color: var(--card-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--card-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-header {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-title a {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-title a:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-meta,
|
||||
[data-theme="dark"] .entry-footer {
|
||||
color: var(--text-secondary) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 搜索框修复 */
|
||||
[data-theme="dark"] #searchform,
|
||||
[data-theme="dark"] .search-form {
|
||||
background-color: var(--input-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #s,
|
||||
[data-theme="dark"] .search-field {
|
||||
background-color: var(--input-bg) !important;
|
||||
color: var(--input-text) !important;
|
||||
border-color: var(--input-border) !important;
|
||||
}
|
||||
|
||||
/* 分页修复 */
|
||||
[data-theme="dark"] .pagination,
|
||||
[data-theme="dark"] .page-links,
|
||||
[data-theme="dark"] .nav-links {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination a,
|
||||
[data-theme="dark"] .page-links a,
|
||||
[data-theme="dark"] .nav-links a {
|
||||
background-color: var(--card-bg) !important;
|
||||
color: var(--link-color) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination a:hover,
|
||||
[data-theme="dark"] .page-links a:hover,
|
||||
[data-theme="dark"] .nav-links a:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination .current,
|
||||
[data-theme="dark"] .page-links > .current {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* 面包屑导航修复 */
|
||||
[data-theme="dark"] .breadcrumbs,
|
||||
[data-theme="dark"] .breadcrumb {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 侧边栏小工具特定修复 */
|
||||
[data-theme="dark"] #calendar_wrap {
|
||||
background-color: var(--card-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #calendar_wrap table,
|
||||
[data-theme="dark"] #calendar_wrap th,
|
||||
[data-theme="dark"] #calendar_wrap td {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 标签云修复 */
|
||||
[data-theme="dark"] .tagcloud a,
|
||||
[data-theme="dark"] .wp_widget_tag_cloud a {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tagcloud a:hover,
|
||||
[data-theme="dark"] .wp_widget_tag_cloud a:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 最近评论修复 */
|
||||
[data-theme="dark"] .recentcomments {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* RSS 链接修复 */
|
||||
[data-theme="dark"] .rss-date,
|
||||
[data-theme="dark"] .rssSummary {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 存档页面修复 */
|
||||
[data-theme="dark"] .archive-meta,
|
||||
[data-theme="dark"] .page-header {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 404 页面修复 */
|
||||
[data-theme="dark"] .error404 .widget {
|
||||
background-color: var(--card-bg) !important;
|
||||
}
|
||||
|
||||
/* 图片说明修复 */
|
||||
[data-theme="dark"] .wp-caption,
|
||||
[data-theme="dark"] .gallery-caption {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wp-caption-text {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 嵌入内容修复 */
|
||||
[data-theme="dark"] embed,
|
||||
[data-theme="dark"] iframe,
|
||||
[data-theme="dark"] object {
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 按钮修复 - 确保按钮上的白色文字不被改变 */
|
||||
[data-theme="dark"] .btn,
|
||||
[data-theme="dark"] button,
|
||||
[data-theme="dark"] input[type="submit"],
|
||||
[data-theme="dark"] input[type="button"],
|
||||
[data-theme="dark"] .comment-reply-link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-primary,
|
||||
[data-theme="dark"] .btn-success,
|
||||
[data-theme="dark"] .btn-info,
|
||||
[data-theme="dark"] .btn-warning,
|
||||
[data-theme="dark"] .btn-danger {
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* Sticky post 修复 */
|
||||
[data-theme="dark"] .sticky {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
/* 引用文字修复 */
|
||||
[data-theme="dark"] cite {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 列表修复 */
|
||||
[data-theme="dark"] ul,
|
||||
[data-theme="dark"] ol,
|
||||
[data-theme="dark"] dl {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 定义列表修复 */
|
||||
[data-theme="dark"] dt {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] dd {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 强调文本修复 */
|
||||
[data-theme="dark"] strong,
|
||||
[data-theme="dark"] b {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] em,
|
||||
[data-theme="dark"] i {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 删除线修复 */
|
||||
[data-theme="dark"] del,
|
||||
[data-theme="dark"] s {
|
||||
color: var(--text-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 下划线修复 */
|
||||
[data-theme="dark"] ins,
|
||||
[data-theme="dark"] u {
|
||||
color: var(--text-primary) !important;
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 小号文字修复 */
|
||||
[data-theme="dark"] small {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 标记文字修复 */
|
||||
[data-theme="dark"] mark {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Pygments 代码高亮修复 */
|
||||
[data-theme="dark"] .highlight,
|
||||
[data-theme="dark"] .codehilite {
|
||||
background-color: var(--code-block-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .highlight pre,
|
||||
[data-theme="dark"] .codehilite pre {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 站点标题和描述修复 */
|
||||
[data-theme="dark"] .site-title,
|
||||
[data-theme="dark"] .site-description {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-title a {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-title a:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 页面容器修复 */
|
||||
[data-theme="dark"] #page,
|
||||
[data-theme="dark"] .site,
|
||||
[data-theme="dark"] #main,
|
||||
[data-theme="dark"] .wrapper {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复特定的警告框背景 */
|
||||
[data-theme="dark"] *[style*="background: #fff9c0"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff9c0"] {
|
||||
background-color: rgba(255, 249, 192, 0.2) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复特定的警告框背景 */
|
||||
[data-theme="dark"] *[style*="background: #fff3cd"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff3cd"] {
|
||||
background-color: rgba(255, 243, 205, 0.2) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:文章卡片内部元素修复 */
|
||||
[data-theme="dark"] .post-thumbnail,
|
||||
[data-theme="dark"] .entry-thumbnail {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 补充:作者信息框修复 */
|
||||
[data-theme="dark"] .author-info,
|
||||
[data-theme="dark"] .author-bio {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:相关文章修复 */
|
||||
[data-theme="dark"] .related-posts,
|
||||
[data-theme="dark"] .related-articles {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* 补充:分类和标签显示修复 */
|
||||
[data-theme="dark"] .cat-links,
|
||||
[data-theme="dark"] .tags-links {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .cat-links a,
|
||||
[data-theme="dark"] .tags-links a {
|
||||
color: var(--link-color) !important;
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 补充:阅读更多链接修复 */
|
||||
[data-theme="dark"] .more-link {
|
||||
color: var(--link-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .more-link:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 补充:表单元素标签修复 */
|
||||
[data-theme="dark"] label {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:占位符修复 */
|
||||
[data-theme="dark"] ::placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] :-ms-input-placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-ms-input-placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
}
|
||||
|
||||
/* 补充:选中文本修复 */
|
||||
[data-theme="dark"] ::selection {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-moz-selection {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* 修复 hfeed 类容器 */
|
||||
[data-theme="dark"] .hfeed {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有可能的白色背景覆盖 */
|
||||
[data-theme="dark"] .site-header,
|
||||
[data-theme="dark"] .site-content,
|
||||
[data-theme="dark"] .site-footer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import django
|
||||
import os
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoblog.settings')
|
||||
django.setup()
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
print("=" * 60)
|
||||
print("检查 Django Admin 注册情况")
|
||||
print("=" * 60)
|
||||
|
||||
blog_models = []
|
||||
for model in admin.site._registry.keys():
|
||||
if model._meta.app_label == 'blog':
|
||||
blog_models.append(model)
|
||||
|
||||
print(f"\n已注册的 Blog 模型数量: {len(blog_models)}")
|
||||
print("\n模型列表:")
|
||||
for model in sorted(blog_models, key=lambda m: m.__name__):
|
||||
verbose = model._meta.verbose_name
|
||||
verbose_plural = model._meta.verbose_name_plural
|
||||
print(f" {model.__name__}")
|
||||
print(f" 显示名称: {verbose} / {verbose_plural}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
import django
|
||||
import os
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoblog.settings')
|
||||
django.setup()
|
||||
|
||||
from djangoblog.admin_site import admin_site
|
||||
|
||||
print("=" * 60)
|
||||
print("检查自定义 Admin Site 注册情况")
|
||||
print("=" * 60)
|
||||
|
||||
blog_models = []
|
||||
for model in admin_site._registry.keys():
|
||||
if model._meta.app_label == 'blog':
|
||||
blog_models.append(model)
|
||||
|
||||
print(f"\n已注册的 Blog 模型数量: {len(blog_models)}")
|
||||
print("\n模型列表:")
|
||||
for model in sorted(blog_models, key=lambda m: m.__name__):
|
||||
verbose = model._meta.verbose_name
|
||||
verbose_plural = model._meta.verbose_name_plural
|
||||
print(f" {model.__name__}")
|
||||
print(f" 显示名称: {verbose} / {verbose_plural}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
@ -0,0 +1 @@
|
||||
# Management commands module
|
||||
@ -0,0 +1 @@
|
||||
# Management commands
|
||||
@ -0,0 +1,23 @@
|
||||
# 性能优化:为评论模型添加数据库索引
|
||||
# Generated manually for performance optimization
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 为 Comment 模型添加组合索引
|
||||
migrations.AddIndex(
|
||||
model_name='comment',
|
||||
index=models.Index(fields=['article', 'is_enable'], name='comment_article_enable_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='comment',
|
||||
index=models.Index(fields=['is_enable', '-id'], name='comment_enable_id_idx'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,14 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-24 02:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0002_add_performance_indexes'),
|
||||
('comments', '0003_alter_comment_options_remove_comment_created_time_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-25 13:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0004_merge_20251124_0221'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='comment',
|
||||
name='comment_article_enable_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='comment',
|
||||
name='comment_enable_id_idx',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,457 @@
|
||||
/* 深色模式修复 - 覆盖 style.css 中的硬编码白色背景 */
|
||||
|
||||
/* 覆盖所有白色背景为使用CSS变量 */
|
||||
[data-theme="dark"] .site-header,
|
||||
[data-theme="dark"] #masthead {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-content,
|
||||
[data-theme="dark"] #content {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget-area,
|
||||
[data-theme="dark"] #secondary {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-content,
|
||||
[data-theme="dark"] .entry-summary,
|
||||
[data-theme="dark"] .page-content,
|
||||
[data-theme="dark"] article {
|
||||
background-color: var(--article-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] body {
|
||||
background-color: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有白色背景的元素 */
|
||||
[data-theme="dark"] *[style*="background: #fff"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff"],
|
||||
[data-theme="dark"] *[style*="background: white"],
|
||||
[data-theme="dark"] *[style*="background-color: white"] {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有白色文字的元素(排除按钮和链接) */
|
||||
[data-theme="dark"] *[style*="color: #fff"]:not(.btn):not(a),
|
||||
[data-theme="dark"] *[style*="color: white"]:not(.btn):not(a) {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 评论区修复 */
|
||||
[data-theme="dark"] #comments,
|
||||
[data-theme="dark"] .comment-list,
|
||||
[data-theme="dark"] .comment,
|
||||
[data-theme="dark"] .comment-body,
|
||||
[data-theme="dark"] .comment-content {
|
||||
background-color: var(--comment-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--comment-border) !important;
|
||||
}
|
||||
|
||||
/* 导航菜单修复 */
|
||||
[data-theme="dark"] .nav-menu,
|
||||
[data-theme="dark"] .main-navigation,
|
||||
[data-theme="dark"] #site-navigation {
|
||||
background-color: var(--nav-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu li,
|
||||
[data-theme="dark"] .main-navigation li {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu a,
|
||||
[data-theme="dark"] .main-navigation a {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-menu a:hover,
|
||||
[data-theme="dark"] .main-navigation a:hover {
|
||||
background-color: var(--nav-hover-bg) !important;
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* Widget 修复 */
|
||||
[data-theme="dark"] .widget {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--sidebar-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget-title {
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget ul,
|
||||
[data-theme="dark"] .widget ol {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .widget a {
|
||||
color: var(--link-color) !important;
|
||||
}
|
||||
|
||||
/* 文章列表修复 */
|
||||
[data-theme="dark"] .hentry,
|
||||
[data-theme="dark"] .post,
|
||||
[data-theme="dark"] .page {
|
||||
background-color: var(--card-bg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--card-border) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-header {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-title a {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-title a:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .entry-meta,
|
||||
[data-theme="dark"] .entry-footer {
|
||||
color: var(--text-secondary) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 搜索框修复 */
|
||||
[data-theme="dark"] #searchform,
|
||||
[data-theme="dark"] .search-form {
|
||||
background-color: var(--input-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #s,
|
||||
[data-theme="dark"] .search-field {
|
||||
background-color: var(--input-bg) !important;
|
||||
color: var(--input-text) !important;
|
||||
border-color: var(--input-border) !important;
|
||||
}
|
||||
|
||||
/* 分页修复 */
|
||||
[data-theme="dark"] .pagination,
|
||||
[data-theme="dark"] .page-links,
|
||||
[data-theme="dark"] .nav-links {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination a,
|
||||
[data-theme="dark"] .page-links a,
|
||||
[data-theme="dark"] .nav-links a {
|
||||
background-color: var(--card-bg) !important;
|
||||
color: var(--link-color) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination a:hover,
|
||||
[data-theme="dark"] .page-links a:hover,
|
||||
[data-theme="dark"] .nav-links a:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .pagination .current,
|
||||
[data-theme="dark"] .page-links > .current {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* 面包屑导航修复 */
|
||||
[data-theme="dark"] .breadcrumbs,
|
||||
[data-theme="dark"] .breadcrumb {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 侧边栏小工具特定修复 */
|
||||
[data-theme="dark"] #calendar_wrap {
|
||||
background-color: var(--card-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] #calendar_wrap table,
|
||||
[data-theme="dark"] #calendar_wrap th,
|
||||
[data-theme="dark"] #calendar_wrap td {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 标签云修复 */
|
||||
[data-theme="dark"] .tagcloud a,
|
||||
[data-theme="dark"] .wp_widget_tag_cloud a {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tagcloud a:hover,
|
||||
[data-theme="dark"] .wp_widget_tag_cloud a:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 最近评论修复 */
|
||||
[data-theme="dark"] .recentcomments {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* RSS 链接修复 */
|
||||
[data-theme="dark"] .rss-date,
|
||||
[data-theme="dark"] .rssSummary {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 存档页面修复 */
|
||||
[data-theme="dark"] .archive-meta,
|
||||
[data-theme="dark"] .page-header {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 404 页面修复 */
|
||||
[data-theme="dark"] .error404 .widget {
|
||||
background-color: var(--card-bg) !important;
|
||||
}
|
||||
|
||||
/* 图片说明修复 */
|
||||
[data-theme="dark"] .wp-caption,
|
||||
[data-theme="dark"] .gallery-caption {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wp-caption-text {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 嵌入内容修复 */
|
||||
[data-theme="dark"] embed,
|
||||
[data-theme="dark"] iframe,
|
||||
[data-theme="dark"] object {
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 按钮修复 - 确保按钮上的白色文字不被改变 */
|
||||
[data-theme="dark"] .btn,
|
||||
[data-theme="dark"] button,
|
||||
[data-theme="dark"] input[type="submit"],
|
||||
[data-theme="dark"] input[type="button"],
|
||||
[data-theme="dark"] .comment-reply-link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-primary,
|
||||
[data-theme="dark"] .btn-success,
|
||||
[data-theme="dark"] .btn-info,
|
||||
[data-theme="dark"] .btn-warning,
|
||||
[data-theme="dark"] .btn-danger {
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* Sticky post 修复 */
|
||||
[data-theme="dark"] .sticky {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-color: var(--accent-primary) !important;
|
||||
}
|
||||
|
||||
/* 引用文字修复 */
|
||||
[data-theme="dark"] cite {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 列表修复 */
|
||||
[data-theme="dark"] ul,
|
||||
[data-theme="dark"] ol,
|
||||
[data-theme="dark"] dl {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 定义列表修复 */
|
||||
[data-theme="dark"] dt {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] dd {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 强调文本修复 */
|
||||
[data-theme="dark"] strong,
|
||||
[data-theme="dark"] b {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] em,
|
||||
[data-theme="dark"] i {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 删除线修复 */
|
||||
[data-theme="dark"] del,
|
||||
[data-theme="dark"] s {
|
||||
color: var(--text-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 下划线修复 */
|
||||
[data-theme="dark"] ins,
|
||||
[data-theme="dark"] u {
|
||||
color: var(--text-primary) !important;
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 小号文字修复 */
|
||||
[data-theme="dark"] small {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
/* 标记文字修复 */
|
||||
[data-theme="dark"] mark {
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Pygments 代码高亮修复 */
|
||||
[data-theme="dark"] .highlight,
|
||||
[data-theme="dark"] .codehilite {
|
||||
background-color: var(--code-block-bg) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .highlight pre,
|
||||
[data-theme="dark"] .codehilite pre {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 站点标题和描述修复 */
|
||||
[data-theme="dark"] .site-title,
|
||||
[data-theme="dark"] .site-description {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-title a {
|
||||
color: var(--nav-text) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .site-title a:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 页面容器修复 */
|
||||
[data-theme="dark"] #page,
|
||||
[data-theme="dark"] .site,
|
||||
[data-theme="dark"] #main,
|
||||
[data-theme="dark"] .wrapper {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复特定的警告框背景 */
|
||||
[data-theme="dark"] *[style*="background: #fff9c0"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff9c0"] {
|
||||
background-color: rgba(255, 249, 192, 0.2) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复特定的警告框背景 */
|
||||
[data-theme="dark"] *[style*="background: #fff3cd"],
|
||||
[data-theme="dark"] *[style*="background-color: #fff3cd"] {
|
||||
background-color: rgba(255, 243, 205, 0.2) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:文章卡片内部元素修复 */
|
||||
[data-theme="dark"] .post-thumbnail,
|
||||
[data-theme="dark"] .entry-thumbnail {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 补充:作者信息框修复 */
|
||||
[data-theme="dark"] .author-info,
|
||||
[data-theme="dark"] .author-bio {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: var(--border-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:相关文章修复 */
|
||||
[data-theme="dark"] .related-posts,
|
||||
[data-theme="dark"] .related-articles {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
/* 补充:分类和标签显示修复 */
|
||||
[data-theme="dark"] .cat-links,
|
||||
[data-theme="dark"] .tags-links {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .cat-links a,
|
||||
[data-theme="dark"] .tags-links a {
|
||||
color: var(--link-color) !important;
|
||||
background-color: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
/* 补充:阅读更多链接修复 */
|
||||
[data-theme="dark"] .more-link {
|
||||
color: var(--link-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .more-link:hover {
|
||||
color: var(--link-hover) !important;
|
||||
}
|
||||
|
||||
/* 补充:表单元素标签修复 */
|
||||
[data-theme="dark"] label {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* 补充:占位符修复 */
|
||||
[data-theme="dark"] ::placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-theme="dark"] :-ms-input-placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-ms-input-placeholder {
|
||||
color: var(--input-placeholder) !important;
|
||||
}
|
||||
|
||||
/* 补充:选中文本修复 */
|
||||
[data-theme="dark"] ::selection {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-moz-selection {
|
||||
background-color: var(--accent-primary) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
}
|
||||
|
||||
/* 修复 hfeed 类容器 */
|
||||
[data-theme="dark"] .hfeed {
|
||||
background-color: var(--bg-primary) !important;
|
||||
}
|
||||
|
||||
/* 修复所有可能的白色背景覆盖 */
|
||||
[data-theme="dark"] .site-header,
|
||||
[data-theme="dark"] .site-content,
|
||||
[data-theme="dark"] .site-footer {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
/* 多媒体选择器样式 */
|
||||
|
||||
.media-picker-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.media-picker-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.media-picker-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 1000px;
|
||||
height: 80%;
|
||||
max-height: 700px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.media-picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.media-picker-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.media-picker-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-picker-close:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.media-picker-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.media-picker-search input {
|
||||
padding: 8px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 250px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-upload {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-upload:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.media-picker-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.media-files-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.media-file-item {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.media-file-item:hover {
|
||||
border-color: #4CAF50;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.media-file-item.selected {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.media-file-preview {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.media-file-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.media-file-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-file-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.media-file-meta {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.media-file-check {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #4CAF50;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.media-file-item.selected .media-file-check {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.media-picker-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.media-picker-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.media-picker-pagination button {
|
||||
padding: 6px 15px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.media-picker-pagination button:hover:not(:disabled) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.media-picker-pagination button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.media-picker-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-select {
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-select {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-select:hover:not(:disabled) {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading, .error, .empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.media-picker-container {
|
||||
width: 95%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.media-files-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-file-preview {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.media-picker-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-picker-search input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,750 @@
|
||||
/* 响应式布局优化
|
||||
* 提供完整的移动端、平板和桌面端适配
|
||||
* 基于Bootstrap断点扩展
|
||||
*/
|
||||
|
||||
/* ==================== 断点定义 ==================== */
|
||||
|
||||
/*
|
||||
xs: <576px (超小屏 - 手机竖屏)
|
||||
sm: ≥576px (小屏 - 手机横屏)
|
||||
md: ≥768px (中屏 - 平板竖屏)
|
||||
lg: ≥992px (大屏 - 平板横屏/小笔记本)
|
||||
xl: ≥1200px (超大屏 - 桌面)
|
||||
xxl: ≥1400px (超超大屏 - 大桌面)
|
||||
*/
|
||||
|
||||
/* ==================== 基础布局 ==================== */
|
||||
|
||||
/* 容器最大宽度 */
|
||||
.container {
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 流式容器 */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ==================== 导航栏 ==================== */
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
border: 1px solid var(--border-primary, #e0e0e0);
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* 移动端导航 */
|
||||
@media (max-width: 991px) {
|
||||
.navbar-toggler {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-collapse.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin: 5px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 文章列表 ==================== */
|
||||
|
||||
.article-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.article-list.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.article-list.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 文章卡片 */
|
||||
.article-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-card-img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.article-card-img {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.article-card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.article-card-title {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.article-card-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 侧边栏布局 ==================== */
|
||||
|
||||
.main-content-wrapper {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.main-content-wrapper.with-sidebar {
|
||||
grid-template-columns: 1fr 300px;
|
||||
}
|
||||
|
||||
.main-content-wrapper.sidebar-left {
|
||||
grid-template-columns: 300px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 移动端侧边栏 */
|
||||
@media (max-width: 991px) {
|
||||
.sidebar {
|
||||
order: -1; /* 移到内容上方 */
|
||||
}
|
||||
|
||||
.sidebar-widget {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 表格响应式 ==================== */
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
/* 隐藏不太重要的列 */
|
||||
table .hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 表单 ==================== */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--input-border, #d0d0d0);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.form-control {
|
||||
font-size: 16px; /* 防止iOS自动缩放 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮组响应式 */
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 图片响应式 ==================== */
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 图片网格 */
|
||||
.image-grid {
|
||||
display: grid;
|
||||
gap: 15px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.image-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.image-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 文字排版 ==================== */
|
||||
|
||||
/* 标题大小 */
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 段落间距 */
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 间距工具类 ==================== */
|
||||
|
||||
/* 移动端减少间距 */
|
||||
@media (max-width: 767px) {
|
||||
.mt-md-5 {
|
||||
margin-top: 3rem !important;
|
||||
}
|
||||
|
||||
.mb-md-5 {
|
||||
margin-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.p-md-5 {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.mt-sm-4 {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mb-sm-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.p-sm-4 {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 显示/隐藏工具类 ==================== */
|
||||
|
||||
/* 移动端隐藏 */
|
||||
@media (max-width: 575px) {
|
||||
.hide-xs {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.hide-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.hide-md {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端显示 */
|
||||
.show-xs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.show-xs {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.show-sm {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.show-sm {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 评论区 ==================== */
|
||||
|
||||
.comment-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.comment-reply {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.comment-reply {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 分页 ==================== */
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-primary, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 6px 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pagination .page-text {
|
||||
display: none; /* 隐藏"上一页"/"下一页"文字,只显示箭头 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 模态框 ==================== */
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
margin: 30px auto;
|
||||
max-width: 500px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.modal-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.modal-dialog.modal-lg {
|
||||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.modal-dialog {
|
||||
margin: 10px auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 搜索框 ==================== */
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.search-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-form input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-form button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 标签云 ==================== */
|
||||
|
||||
.tag-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 面包屑 ==================== */
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "/";
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.breadcrumb {
|
||||
font-size: 0.875rem;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 卡片 ==================== */
|
||||
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--border-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 15px;
|
||||
border-top: 1px solid var(--border-primary, #e0e0e0);
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.card-header,
|
||||
.card-body,
|
||||
.card-footer {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 页脚 ==================== */
|
||||
|
||||
footer {
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
footer {
|
||||
padding: 30px 0 15px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 触摸优化 ==================== */
|
||||
|
||||
/* 增大移动端可点击区域 */
|
||||
@media (max-width: 767px) {
|
||||
a,
|
||||
button,
|
||||
.clickable {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-link,
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 性能优化 ==================== */
|
||||
|
||||
/* GPU加速 */
|
||||
.transform-gpu {
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
/* 减少重绘 */
|
||||
img,
|
||||
video {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ==================== 打印样式 ==================== */
|
||||
|
||||
@media print {
|
||||
.no-print,
|
||||
.navbar,
|
||||
.sidebar,
|
||||
.comments,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a[href]:after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 用户关注和文章收藏功能
|
||||
* 提供关注/取消关注、收藏/取消收藏的前端交互
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 获取CSRF Token
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// 社交功能类
|
||||
class SocialFeatures {
|
||||
constructor() {
|
||||
this.csrfToken = getCookie('csrftoken');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 绑定关注按钮
|
||||
this.bindFollowButtons();
|
||||
// 绑定收藏按钮
|
||||
this.bindFavoriteButtons();
|
||||
// 绑定点赞按钮
|
||||
this.bindLikeButtons();
|
||||
}
|
||||
|
||||
// ==================== 关注功能 ====================
|
||||
|
||||
bindFollowButtons() {
|
||||
const followButtons = document.querySelectorAll('.btn-follow');
|
||||
followButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const userId = button.dataset.userId;
|
||||
const isFollowing = button.dataset.following === 'true';
|
||||
|
||||
if (isFollowing) {
|
||||
this.unfollowUser(userId, button);
|
||||
} else {
|
||||
this.followUser(userId, button);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
followUser(userId, button) {
|
||||
fetch('/blog/api/follow/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.textContent = '已关注';
|
||||
button.dataset.following = 'true';
|
||||
button.classList.remove('btn-primary');
|
||||
button.classList.add('btn-secondary');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateFollowCount(data.following_count);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('关注失败:', error);
|
||||
this.showMessage('关注失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
unfollowUser(userId, button) {
|
||||
fetch('/blog/api/unfollow/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.textContent = '关注';
|
||||
button.dataset.following = 'false';
|
||||
button.classList.remove('btn-secondary');
|
||||
button.classList.add('btn-primary');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateFollowCount(data.following_count);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('取消关注失败:', error);
|
||||
this.showMessage('取消关注失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
updateFollowCount(count) {
|
||||
const countElement = document.querySelector('#following-count');
|
||||
if (countElement) {
|
||||
countElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 收藏功能 ====================
|
||||
|
||||
bindFavoriteButtons() {
|
||||
const favoriteButtons = document.querySelectorAll('.btn-favorite');
|
||||
favoriteButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const articleId = button.dataset.articleId;
|
||||
const isFavorited = button.dataset.favorited === 'true';
|
||||
|
||||
if (isFavorited) {
|
||||
this.unfavoriteArticle(articleId, button);
|
||||
} else {
|
||||
this.favoriteArticle(articleId, button);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
favoriteArticle(articleId, button) {
|
||||
fetch('/blog/api/favorite/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ article_id: articleId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '★ 已收藏';
|
||||
button.dataset.favorited = 'true';
|
||||
button.classList.add('favorited');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateFavoriteCount(data.favorite_count);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('收藏失败:', error);
|
||||
this.showMessage('收藏失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
unfavoriteArticle(articleId, button) {
|
||||
fetch('/blog/api/unfavorite/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ article_id: articleId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '☆ 收藏';
|
||||
button.dataset.favorited = 'false';
|
||||
button.classList.remove('favorited');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateFavoriteCount(data.favorite_count);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('取消收藏失败:', error);
|
||||
this.showMessage('取消收藏失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
updateFavoriteCount(count) {
|
||||
const countElement = document.querySelector('#favorite-count');
|
||||
if (countElement) {
|
||||
countElement.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 点赞功能 ====================
|
||||
|
||||
bindLikeButtons() {
|
||||
const likeButtons = document.querySelectorAll('.btn-like');
|
||||
likeButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const articleId = button.dataset.articleId;
|
||||
const isLiked = button.dataset.liked === 'true';
|
||||
|
||||
if (isLiked) {
|
||||
this.unlikeArticle(articleId, button);
|
||||
} else {
|
||||
this.likeArticle(articleId, button);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
likeArticle(articleId, button) {
|
||||
fetch('/blog/api/like/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ article_id: articleId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '👍 ' + data.like_count;
|
||||
button.dataset.liked = 'true';
|
||||
button.classList.add('liked');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateLikeCount(data.like_count, button);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('点赞失败:', error);
|
||||
this.showMessage('点赞失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
unlikeArticle(articleId, button) {
|
||||
fetch('/blog/api/unlike/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': this.csrfToken
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ article_id: articleId })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
button.innerHTML = '👍 ' + data.like_count;
|
||||
button.dataset.liked = 'false';
|
||||
button.classList.remove('liked');
|
||||
this.showMessage(data.message, 'success');
|
||||
this.updateLikeCount(data.like_count, button);
|
||||
} else {
|
||||
this.showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('取消点赞失败:', error);
|
||||
this.showMessage('取消点赞失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
updateLikeCount(count, button) {
|
||||
// 更新按钮显示的点赞数
|
||||
const countSpan = button.querySelector('.like-count');
|
||||
if (countSpan) {
|
||||
countSpan.textContent = count;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
showMessage(message, type) {
|
||||
// 创建或获取消息提示元素
|
||||
let messageBox = document.querySelector('#social-message');
|
||||
if (!messageBox) {
|
||||
messageBox = document.createElement('div');
|
||||
messageBox.id = 'social-message';
|
||||
messageBox.style.cssText = 'position: fixed; top: 20px; right: 20px; padding: 15px 20px; ' +
|
||||
'border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); ' +
|
||||
'font-size: 14px; z-index: 9999; display: none;';
|
||||
document.body.appendChild(messageBox);
|
||||
}
|
||||
|
||||
const colors = {
|
||||
'success': '#4CAF50',
|
||||
'error': '#F44336',
|
||||
'info': '#2196F3'
|
||||
};
|
||||
|
||||
messageBox.textContent = message;
|
||||
messageBox.style.background = colors[type] || '#f0f0f0';
|
||||
messageBox.style.color = '#fff';
|
||||
messageBox.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
messageBox.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.socialFeatures = new SocialFeatures();
|
||||
});
|
||||
} else {
|
||||
window.socialFeatures = new SocialFeatures();
|
||||
}
|
||||
|
||||
})();
|
||||
@ -0,0 +1,340 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.compare-container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.summary-box {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-box h3 {
|
||||
margin-top: 0;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.change-list li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.change-list li.changed {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.change-list li.changed:before {
|
||||
content: "✏️ ";
|
||||
}
|
||||
|
||||
.change-list li.unchanged {
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.change-list li.unchanged:before {
|
||||
content: "✓ ";
|
||||
}
|
||||
|
||||
.comparison-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.comparison-section h3 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.compare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.compare-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.compare-item h4 {
|
||||
margin-top: 0;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.compare-item .value {
|
||||
margin-top: 10px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.diff-highlight {
|
||||
background: #fff3cd;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-restore {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-restore:hover {
|
||||
background: #218838;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #5a6268;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 差异表格样式 */
|
||||
.diff-section {
|
||||
margin-top: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.diff-section table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diff-section table td {
|
||||
padding: 2px 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.diff-section .diff_header {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.diff-section .diff_next {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.diff-section .diff_add {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.diff-section .diff_chg {
|
||||
background: #fff3cd;
|
||||
}
|
||||
|
||||
.diff-section .diff_sub {
|
||||
background: #f8d7da;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:blog_articleversion_changelist' %}">文章版本</a>
|
||||
› 对比版本 v{{ version.version_number }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="compare-container">
|
||||
<h1>版本对比</h1>
|
||||
|
||||
<div class="summary-box">
|
||||
<h3>📊 变更摘要</h3>
|
||||
<ul class="change-list">
|
||||
<li class="{% if diff.title_changed %}changed{% else %}unchanged{% endif %}">
|
||||
标题{% if diff.title_changed %} 已修改{% else %} 未变更{% endif %}
|
||||
</li>
|
||||
<li class="{% if diff.body_changed %}changed{% else %}unchanged{% endif %}">
|
||||
正文{% if diff.body_changed %} 已修改{% else %} 未变更{% endif %}
|
||||
</li>
|
||||
<li class="{% if diff.status_changed %}changed{% else %}unchanged{% endif %}">
|
||||
发布状态{% if diff.status_changed %} 已修改{% else %} 未变更{% endif %}
|
||||
</li>
|
||||
<li class="{% if diff.category_changed %}changed{% else %}unchanged{% endif %}">
|
||||
分类{% if diff.category_changed %} 已修改{% else %} 未变更{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<h3>基本信息对比</h3>
|
||||
<div class="compare-grid">
|
||||
<div class="compare-item">
|
||||
<h4>版本 v{{ version.version_number }}</h4>
|
||||
<div class="value">
|
||||
<strong>标题:</strong><br>
|
||||
{% if diff.title_changed %}<span class="diff-highlight">{{ version.title }}</span>
|
||||
{% else %}{{ version.title }}{% endif %}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>创建时间:</strong><br>
|
||||
{{ version.creation_time|date:"Y-m-d H:i:s" }}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>创建者:</strong><br>
|
||||
{{ version.created_by|default:"系统" }}
|
||||
</div>
|
||||
{% if version.change_summary %}
|
||||
<div class="value">
|
||||
<strong>变更说明:</strong><br>
|
||||
{{ version.change_summary }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="compare-item">
|
||||
<h4>当前版本</h4>
|
||||
<div class="value">
|
||||
<strong>标题:</strong><br>
|
||||
{% if diff.title_changed %}<span class="diff-highlight">{{ current.title }}</span>
|
||||
{% else %}{{ current.title }}{% endif %}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>最后修改:</strong><br>
|
||||
{{ current.last_mod_time|date:"Y-m-d H:i:s" }}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>作者:</strong><br>
|
||||
{{ current.author }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<h3>状态信息对比</h3>
|
||||
<div class="compare-grid">
|
||||
<div class="compare-item">
|
||||
<h4>版本 v{{ version.version_number }}</h4>
|
||||
<div class="value">
|
||||
<strong>发布状态:</strong><br>
|
||||
{% if diff.status_changed %}<span class="diff-highlight">
|
||||
{% endif %}
|
||||
{% if version.status == 'p' %}已发布
|
||||
{% elif version.status == 'd' %}草稿
|
||||
{% else %}{{ version.status }}
|
||||
{% endif %}
|
||||
{% if diff.status_changed %}</span>{% endif %}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>评论状态:</strong><br>
|
||||
{% if version.comment_status == 'o' %}开放
|
||||
{% elif version.comment_status == 'c' %}关闭
|
||||
{% else %}{{ version.comment_status }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-item">
|
||||
<h4>当前版本</h4>
|
||||
<div class="value">
|
||||
<strong>发布状态:</strong><br>
|
||||
{% if diff.status_changed %}<span class="diff-highlight">
|
||||
{% endif %}
|
||||
{% if current.status == 'p' %}已发布
|
||||
{% elif current.status == 'd' %}草稿
|
||||
{% else %}{{ current.status }}
|
||||
{% endif %}
|
||||
{% if diff.status_changed %}</span>{% endif %}
|
||||
</div>
|
||||
<div class="value">
|
||||
<strong>评论状态:</strong><br>
|
||||
{% if current.comment_status == 'o' %}开放
|
||||
{% elif current.comment_status == 'c' %}关闭
|
||||
{% else %}{{ current.comment_status }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-section">
|
||||
<h3>分类对比</h3>
|
||||
<div class="compare-grid">
|
||||
<div class="compare-item">
|
||||
<h4>版本 v{{ version.version_number }}</h4>
|
||||
<div class="value">
|
||||
{% if diff.category_changed %}<span class="diff-highlight">{{ version.category_name }}</span>
|
||||
{% else %}{{ version.category_name }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="compare-item">
|
||||
<h4>当前版本</h4>
|
||||
<div class="value">
|
||||
{% if diff.category_changed %}<span class="diff-highlight">{{ current.category.name }}</span>
|
||||
{% else %}{{ current.category.name }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if diff.body_changed and body_diff_html %}
|
||||
<div class="comparison-section">
|
||||
<h3>正文内容详细对比</h3>
|
||||
<p style="color: #666; font-size: 13px; margin-bottom: 10px;">
|
||||
🟨 黄色背景 = 修改的行 | 🟩 绿色背景 = 新增的行 | 🟥 红色背景 = 删除的行
|
||||
</p>
|
||||
<div class="diff-section">
|
||||
{{ body_diff_html|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="action-buttons">
|
||||
<a href="{% url 'admin:restore_article_version' version.id %}" class="btn-restore">🔄 恢复此版本</a>
|
||||
<a href="{% url 'admin:blog_articleversion_changelist' %}" class="btn-back">返回列表</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -0,0 +1,199 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrastyle %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.restore-container {
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.warning-box h3 {
|
||||
margin-top: 0;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.version-info h3 {
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.content-preview {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-restore {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn-restore:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-compare {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-compare:hover {
|
||||
background: #0056b3;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:blog_articleversion_changelist' %}">文章版本</a>
|
||||
› 恢复版本 v{{ version.version_number }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="restore-container">
|
||||
<h1>恢复文章版本</h1>
|
||||
|
||||
<div class="warning-box">
|
||||
<h3>⚠️ 重要提示</h3>
|
||||
<ul>
|
||||
<li>恢复版本会将文章内容替换为所选版本的内容</li>
|
||||
<li>当前版本的内容会被自动保存到历史记录中</li>
|
||||
<li>此操作可以撤销(通过恢复到其他版本)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="version-info">
|
||||
<h3>版本信息</h3>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-label">版本号:</div>
|
||||
<div>v{{ version.version_number }}</div>
|
||||
|
||||
<div class="info-label">标题:</div>
|
||||
<div>{{ version.title }}</div>
|
||||
|
||||
<div class="info-label">创建时间:</div>
|
||||
<div>{{ version.creation_time|date:"Y-m-d H:i:s" }}</div>
|
||||
|
||||
<div class="info-label">创建者:</div>
|
||||
<div>{{ version.created_by|default:"系统" }}</div>
|
||||
|
||||
<div class="info-label">变更说明:</div>
|
||||
<div>{{ version.change_summary|default:"无" }}</div>
|
||||
|
||||
<div class="info-label">分类:</div>
|
||||
<div>{{ version.category_name }}</div>
|
||||
|
||||
<div class="info-label">状态:</div>
|
||||
<div>
|
||||
{% if version.status == 'p' %}已发布
|
||||
{% elif version.status == 'd' %}草稿
|
||||
{% else %}{{ version.status }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong class="info-label">内容预览:</strong>
|
||||
<div class="content-preview">
|
||||
{{ version.body|truncatewords:200|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version-info">
|
||||
<h3>当前版本信息</h3>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-label">标题:</div>
|
||||
<div>{{ current.title }}</div>
|
||||
|
||||
<div class="info-label">最后修改:</div>
|
||||
<div>{{ current.last_mod_time|date:"Y-m-d H:i:s" }}</div>
|
||||
|
||||
<div class="info-label">分类:</div>
|
||||
<div>{{ current.category.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="action-buttons">
|
||||
<button type="submit" class="btn-restore">✓ 确认恢复此版本</button>
|
||||
<a href="{% url 'admin:compare_article_version' version.id %}" class="btn-compare">📊 查看详细对比</a>
|
||||
<a href="{% url 'admin:blog_articleversion_changelist' %}" class="btn-cancel">取消</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Reference in new issue