Initial commit: Django gallery project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
gallery/__init__.py
Normal file
0
gallery/__init__.py
Normal file
160
gallery/admin.py
Normal file
160
gallery/admin.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from .models import Artwork, Category, About, Comment
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'slug', 'created_at', 'updated_at')
|
||||
list_filter = ('created_at', 'updated_at')
|
||||
search_fields = ('name', 'slug')
|
||||
prepopulated_fields = {'slug': ('name',)}
|
||||
ordering = ('name',)
|
||||
|
||||
|
||||
class ArtworkAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'title',
|
||||
'category',
|
||||
'order',
|
||||
'view_count',
|
||||
'created_at',
|
||||
'thumbnail_preview'
|
||||
)
|
||||
list_filter = ('category', 'created_at')
|
||||
search_fields = ('title', 'description', 'slug')
|
||||
list_editable = ('order',)
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
ordering = ('order', '-created_at')
|
||||
readonly_fields = ('view_count', 'created_at', 'updated_at', 'image_preview')
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('title', 'slug', 'description', 'category')
|
||||
}),
|
||||
('图片设置', {
|
||||
'fields': ('image', 'image_preview', 'thumbnail')
|
||||
}),
|
||||
('排序与统计', {
|
||||
'fields': ('order', 'view_count')
|
||||
}),
|
||||
('时间信息', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def thumbnail_preview(self, obj):
|
||||
if obj.thumbnail:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-height: 50px; max-width: 50px;" />',
|
||||
obj.thumbnail.url
|
||||
)
|
||||
return "-"
|
||||
thumbnail_preview.short_description = '缩略图'
|
||||
|
||||
def image_preview(self, obj):
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-height: 200px; max-width: 200px;" />',
|
||||
obj.image.url
|
||||
)
|
||||
return "-"
|
||||
image_preview.short_description = '图片预览'
|
||||
|
||||
|
||||
class AboutAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'created_at', 'updated_at')
|
||||
readonly_fields = ('created_at', 'updated_at', 'image_preview')
|
||||
fieldsets = (
|
||||
('内容', {
|
||||
'fields': ('title', 'content', 'image')
|
||||
}),
|
||||
('预览', {
|
||||
'fields': ('image_preview',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('时间信息', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def image_preview(self, obj):
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-height: 200px; max-width: 200px;" />',
|
||||
obj.image.url
|
||||
)
|
||||
return "-"
|
||||
image_preview.short_description = '图片预览'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# 只允许有一个关于页面
|
||||
if About.objects.exists():
|
||||
return False
|
||||
return super().has_add_permission(request)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
# 不允许删除关于页面
|
||||
return False
|
||||
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
"""评论管理"""
|
||||
|
||||
list_display = ['id', 'user', 'artwork', 'text_preview', 'has_image', 'created_at', 'is_active']
|
||||
list_filter = ['artwork', 'user', 'is_active', 'created_at']
|
||||
search_fields = ['text', 'user__username', 'artwork__title']
|
||||
list_editable = ['is_active']
|
||||
list_per_page = 20
|
||||
|
||||
fieldsets = (
|
||||
('基本信息', {
|
||||
'fields': ('artwork', 'user', 'is_active')
|
||||
}),
|
||||
('评论内容', {
|
||||
'fields': ('text', 'image')
|
||||
}),
|
||||
('时间信息', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
def text_preview(self, obj):
|
||||
"""文本内容预览"""
|
||||
if obj.text:
|
||||
return obj.text[:50] + '...' if len(obj.text) > 50 else obj.text
|
||||
return '-'
|
||||
text_preview.short_description = '评论内容'
|
||||
|
||||
def has_image(self, obj):
|
||||
"""是否有图片"""
|
||||
return bool(obj.image)
|
||||
has_image.short_description = '有图片'
|
||||
has_image.boolean = True
|
||||
|
||||
actions = ['activate_comments', 'deactivate_comments']
|
||||
|
||||
def activate_comments(self, request, queryset):
|
||||
"""批量激活评论"""
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'已激活 {updated} 条评论')
|
||||
activate_comments.short_description = '激活选中评论'
|
||||
|
||||
def deactivate_comments(self, request, queryset):
|
||||
"""批量停用评论"""
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'已停用 {updated} 条评论')
|
||||
deactivate_comments.short_description = '停用选中评论'
|
||||
|
||||
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
admin.site.register(Artwork, ArtworkAdmin)
|
||||
admin.site.register(About, AboutAdmin)
|
||||
admin.site.register(Comment, CommentAdmin)
|
||||
|
||||
# 自定义管理站点标题
|
||||
admin.site.site_header = 'Yitao-Ren Gallery 管理后台'
|
||||
admin.site.site_title = '画廊管理'
|
||||
admin.site.index_title = '欢迎使用画廊管理后台'
|
||||
7
gallery/apps.py
Normal file
7
gallery/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GalleryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'gallery'
|
||||
verbose_name = '画廊管理'
|
||||
12
gallery/context_processors.py
Normal file
12
gallery/context_processors.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.conf import settings
|
||||
from .models import Category
|
||||
|
||||
|
||||
def site_settings(request):
|
||||
"""站点设置上下文处理器"""
|
||||
categories = Category.objects.all()
|
||||
return {
|
||||
'site_name': settings.SITE_NAME,
|
||||
'copyright_text': settings.COPYRIGHT_TEXT,
|
||||
'categories': categories,
|
||||
}
|
||||
129
gallery/forms.py
Normal file
129
gallery/forms.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from django import forms
|
||||
import os
|
||||
from .models import Artwork, Category, About, Comment
|
||||
|
||||
|
||||
class ArtworkForm(forms.ModelForm):
|
||||
"""作品表单"""
|
||||
class Meta:
|
||||
model = Artwork
|
||||
fields = [
|
||||
'title',
|
||||
'description',
|
||||
'image',
|
||||
'category',
|
||||
'order',
|
||||
]
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'placeholder': '请输入作品标题'
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'rows': 4,
|
||||
'placeholder': '请输入作品描述'
|
||||
}),
|
||||
'image': forms.FileInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||
}),
|
||||
'category': forms.Select(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||
}),
|
||||
'order': forms.NumberInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'min': 0
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class CategoryForm(forms.ModelForm):
|
||||
"""分类表单"""
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ['name']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'placeholder': '请输入分类名称'
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class AboutForm(forms.ModelForm):
|
||||
"""关于页面表单"""
|
||||
class Meta:
|
||||
model = About
|
||||
fields = ['title', 'content', 'image']
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'placeholder': '请输入标题'
|
||||
}),
|
||||
'content': forms.Textarea(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'rows': 8,
|
||||
'placeholder': '请输入内容'
|
||||
}),
|
||||
'image': forms.FileInput(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500'
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
"""搜索表单"""
|
||||
q = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'w-full px-4 py-2 bg-gray-800 text-white rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'placeholder': '搜索作品...',
|
||||
'autocomplete': 'off'
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
"""评论表单"""
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['text', 'image']
|
||||
widgets = {
|
||||
'text': forms.Textarea(attrs={
|
||||
'class': 'w-full px-3 py-2 border border-gray-700 rounded-lg bg-gray-900 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
|
||||
'rows': 4,
|
||||
'placeholder': '写下您的评论...'
|
||||
}),
|
||||
'image': forms.ClearableFileInput(attrs={
|
||||
'class': 'block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-gray-800 file:text-white hover:file:bg-gray-700',
|
||||
'accept': 'image/*'
|
||||
})
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
"""验证至少填写文本或上传图片"""
|
||||
cleaned_data = super().clean()
|
||||
text = cleaned_data.get('text')
|
||||
image = cleaned_data.get('image')
|
||||
|
||||
if not text and not image:
|
||||
raise forms.ValidationError('请填写评论内容或上传图片')
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_image(self):
|
||||
"""验证上传的图片"""
|
||||
image = self.cleaned_data.get('image')
|
||||
if image:
|
||||
# 文件大小限制(5MB)
|
||||
if image.size > 5 * 1024 * 1024:
|
||||
raise forms.ValidationError('图片大小不能超过5MB')
|
||||
|
||||
# 文件类型验证
|
||||
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
|
||||
ext = os.path.splitext(image.name)[1].lower()
|
||||
if ext not in valid_extensions:
|
||||
raise forms.ValidationError('不支持的文件格式,请上传图片文件')
|
||||
|
||||
return image
|
||||
0
gallery/management/__init__.py
Normal file
0
gallery/management/__init__.py
Normal file
0
gallery/management/commands/__init__.py
Normal file
0
gallery/management/commands/__init__.py
Normal file
144
gallery/management/commands/import_example_images.py
Normal file
144
gallery/management/commands/import_example_images.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import os
|
||||
import random
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files import File
|
||||
from gallery.models import Artwork, Category, About
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '导入示例图片到画廊数据库'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('开始导入示例图片...'))
|
||||
|
||||
# 1. 创建分类
|
||||
categories = self.create_categories()
|
||||
self.stdout.write(self.style.SUCCESS(f'创建了 {len(categories)} 个分类'))
|
||||
|
||||
# 2. 创建关于页面
|
||||
self.create_about_page()
|
||||
self.stdout.write(self.style.SUCCESS('创建了关于页面'))
|
||||
|
||||
# 3. 导入示例图片
|
||||
example_dir = os.path.join(settings.BASE_DIR, 'example_img')
|
||||
if os.path.exists(example_dir):
|
||||
image_files = [f for f in os.listdir(example_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
|
||||
self.stdout.write(self.style.SUCCESS(f'找到 {len(image_files)} 张示例图片'))
|
||||
|
||||
artworks_created = 0
|
||||
for i, filename in enumerate(image_files):
|
||||
try:
|
||||
# 检查是否已经存在相同文件名的作品
|
||||
if Artwork.objects.filter(image__endswith=filename).exists():
|
||||
self.stdout.write(self.style.WARNING(f' 已存在: {filename},跳过'))
|
||||
continue
|
||||
|
||||
artwork = self.create_artwork_from_image(example_dir, filename, i, categories)
|
||||
if artwork:
|
||||
artworks_created += 1
|
||||
self.stdout.write(self.style.SUCCESS(f' 已导入: {artwork.title}'))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f' 导入失败 {filename}: {str(e)}'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'成功导入 {artworks_created} 个作品'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('未找到 example_img 目录,跳过图片导入'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS('示例数据导入完成!'))
|
||||
|
||||
def create_categories(self):
|
||||
"""创建作品分类"""
|
||||
categories_data = [
|
||||
{'name': '风景摄影', 'slug': 'landscape'},
|
||||
{'name': '人像摄影', 'slug': 'portrait'},
|
||||
{'name': '城市建筑', 'slug': 'architecture'},
|
||||
{'name': '自然生态', 'slug': 'nature'},
|
||||
{'name': '抽象艺术', 'slug': 'abstract'},
|
||||
]
|
||||
|
||||
categories = []
|
||||
for data in categories_data:
|
||||
category, created = Category.objects.get_or_create(
|
||||
slug=data['slug'],
|
||||
defaults={'name': data['name']}
|
||||
)
|
||||
if created:
|
||||
categories.append(category)
|
||||
|
||||
return categories
|
||||
|
||||
def create_about_page(self):
|
||||
"""创建关于页面"""
|
||||
if not About.objects.exists():
|
||||
About.objects.create(
|
||||
title='关于 YITAO-REN GALLERY',
|
||||
content='''YITAO-REN GALLERY 是一个专注于现代摄影艺术展示的在线平台。
|
||||
|
||||
我们的使命是发现和推广具有独特视角和艺术价值的摄影作品,为艺术家和艺术爱好者搭建一个交流与展示的空间。
|
||||
|
||||
画廊成立于2026年,名字来源于创始人对于摄影艺术的热爱与追求。"Yitao"代表着艺术的独特道路,"Ren"象征着人文关怀。我们相信,每一幅摄影作品都是摄影师与世界对话的方式,是瞬间与永恒的完美结合。
|
||||
|
||||
在 YITAO-REN GALLERY,您可以:
|
||||
- 欣赏高质量的现代摄影艺术作品
|
||||
- 了解新兴摄影艺术家的创作理念
|
||||
- 参与线上艺术交流活动
|
||||
- 收藏您喜爱的摄影作品
|
||||
|
||||
我们致力于为每一位访问者提供优质的视觉体验,让艺术触手可及。'''
|
||||
)
|
||||
|
||||
def create_artwork_from_image(self, example_dir, filename, index, categories):
|
||||
"""从图片文件创建作品"""
|
||||
# 作品标题和描述
|
||||
titles = [
|
||||
'晨曦微光', '城市脉络', '静谧时光', '光影交错', '自然韵律',
|
||||
'建筑美学', '人文纪实', '色彩交响', '视觉诗篇', '时空印记',
|
||||
'抽象表达', '情感共鸣', '创意无限'
|
||||
]
|
||||
|
||||
descriptions = [
|
||||
'捕捉清晨第一缕阳光洒在大地上的温暖瞬间,展现自然与光的和谐对话。',
|
||||
'现代都市的脉络与节奏,钢筋水泥中的生命律动,城市发展的视觉记录。',
|
||||
'时光在静谧中流淌,记录那些被遗忘的角落和沉淀的记忆。',
|
||||
'光与影的舞蹈,明暗对比中展现物体的立体感和空间层次。',
|
||||
'大自然的韵律与节奏,山川河流的壮美与微观世界的精妙。',
|
||||
'建筑的结构美学与空间哲学,几何线条中的力量与平衡。',
|
||||
'人文关怀的视觉表达,记录时代变迁中的人物与故事。',
|
||||
'色彩的碰撞与融合,视觉上的交响乐,情感的温度计。',
|
||||
'用镜头书写的视觉诗篇,每一帧都是情感的抒发和思想的表达。',
|
||||
'时间与空间的交汇点,记录那些转瞬即逝的珍贵时刻。',
|
||||
'超越具象的视觉语言,用形状、色彩和纹理表达内在情感。',
|
||||
'触动心灵的情感连接,让观者与作品产生深层次的共鸣。',
|
||||
'突破传统的创意表达,探索摄影艺术的无限可能性。'
|
||||
]
|
||||
|
||||
# 选择标题和描述
|
||||
title_index = index % len(titles)
|
||||
description_index = index % len(descriptions)
|
||||
|
||||
# 随机选择分类
|
||||
category = random.choice(categories) if categories else None
|
||||
|
||||
# 根据图片比例智能选择网格尺寸
|
||||
# 这里先使用默认值,图片保存时会根据实际尺寸调整
|
||||
grid_size = 'medium'
|
||||
|
||||
# 创建作品
|
||||
artwork = Artwork(
|
||||
title=titles[title_index],
|
||||
description=descriptions[description_index],
|
||||
order=index * 10, # 按导入顺序排序
|
||||
grid_size=grid_size,
|
||||
category=category
|
||||
)
|
||||
|
||||
# 保存图片
|
||||
image_path = os.path.join(example_dir, filename)
|
||||
with open(image_path, 'rb') as f:
|
||||
artwork.image.save(filename, File(f), save=False)
|
||||
|
||||
# 保存作品
|
||||
artwork.save()
|
||||
|
||||
return artwork
|
||||
68
gallery/migrations/0001_initial.py
Normal file
68
gallery/migrations/0001_initial.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# Generated by Django 4.2.0 on 2026-02-05 10:39
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='About',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='标题')),
|
||||
('content', models.TextField(verbose_name='内容')),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='about/', verbose_name='图片')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '关于页面',
|
||||
'verbose_name_plural': '关于页面',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='分类名称')),
|
||||
('slug', models.SlugField(blank=True, max_length=100, unique=True, verbose_name='URL标识')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '作品分类',
|
||||
'verbose_name_plural': '作品分类',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Artwork',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='作品标题')),
|
||||
('description', models.TextField(blank=True, verbose_name='作品描述')),
|
||||
('slug', models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL标识')),
|
||||
('image', models.ImageField(upload_to='artworks/%Y/%m/%d/', verbose_name='作品图片')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to='thumbnails/%Y/%m/%d/', verbose_name='缩略图')),
|
||||
('order', models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序序号')),
|
||||
('grid_size', models.CharField(choices=[('small', '小 (1x1)'), ('medium', '中 (2x2)'), ('large', '大 (3x3)')], default='medium', help_text='控制首页展示的大小', max_length=10, verbose_name='网格尺寸')),
|
||||
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('view_count', models.PositiveIntegerField(default=0, verbose_name='浏览次数')),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='gallery.category', verbose_name='作品分类')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '摄影作品',
|
||||
'verbose_name_plural': '摄影作品',
|
||||
'ordering': ['order', '-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
17
gallery/migrations/0002_remove_grid_size.py
Normal file
17
gallery/migrations/0002_remove_grid_size.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 4.2 on 2026-02-05 04:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gallery', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='artwork',
|
||||
name='grid_size',
|
||||
),
|
||||
]
|
||||
73
gallery/migrations/0003_comment.py
Normal file
73
gallery/migrations/0003_comment.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Generated by Django 4.2 on 2026-02-16 15:43
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("gallery", "0002_remove_grid_size"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Comment",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("text", models.TextField(blank=True, verbose_name="评论内容")),
|
||||
(
|
||||
"image",
|
||||
models.ImageField(
|
||||
blank=True,
|
||||
null=True,
|
||||
upload_to="comments/%Y/%m/%d/",
|
||||
verbose_name="评论图片",
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(default=True, verbose_name="是否有效"),
|
||||
),
|
||||
(
|
||||
"artwork",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="gallery.artwork",
|
||||
verbose_name="作品",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="用户",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "评论",
|
||||
"verbose_name_plural": "评论",
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
gallery/migrations/__init__.py
Normal file
0
gallery/migrations/__init__.py
Normal file
279
gallery/models.py
Normal file
279
gallery/models.py
Normal file
@@ -0,0 +1,279 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""作品分类模型"""
|
||||
name = models.CharField('分类名称', max_length=100)
|
||||
slug = models.SlugField('URL标识', max_length=100, unique=True, blank=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '作品分类'
|
||||
verbose_name_plural = '作品分类'
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 生成唯一的 slug
|
||||
if not self.slug:
|
||||
base_slug = slugify(self.name)
|
||||
if not base_slug: # 如果 slugify 返回空字符串
|
||||
base_slug = f'category-{self.id}' if self.id else 'category'
|
||||
|
||||
# 确保 slug 唯一
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while Category.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
||||
slug = f'{base_slug}-{counter}'
|
||||
counter += 1
|
||||
self.slug = slug
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Artwork(models.Model):
|
||||
"""摄影作品模型"""
|
||||
|
||||
title = models.CharField('作品标题', max_length=200)
|
||||
description = models.TextField('作品描述', blank=True)
|
||||
slug = models.SlugField('URL标识', max_length=200, unique=True, blank=True)
|
||||
|
||||
# 图片字段
|
||||
image = models.ImageField('作品图片', upload_to='artworks/%Y/%m/%d/')
|
||||
thumbnail = models.ImageField('缩略图', upload_to='thumbnails/%Y/%m/%d/', blank=True, null=True)
|
||||
|
||||
# 关联字段
|
||||
category = models.ForeignKey(
|
||||
Category,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name='作品分类',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# 元数据字段
|
||||
order = models.IntegerField('排序序号', default=0, help_text='数字越小越靠前')
|
||||
|
||||
# 时间字段
|
||||
created_at = models.DateTimeField('创建时间', default=timezone.now)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
# 统计字段
|
||||
view_count = models.PositiveIntegerField('浏览次数', default=0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '摄影作品'
|
||||
verbose_name_plural = '摄影作品'
|
||||
ordering = ['order', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 生成唯一的 slug
|
||||
if not self.slug:
|
||||
base_slug = slugify(self.title)
|
||||
if not base_slug: # 如果 slugify 返回空字符串
|
||||
base_slug = f'artwork-{self.id}' if self.id else 'artwork'
|
||||
|
||||
# 确保 slug 唯一
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while Artwork.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
||||
slug = f'{base_slug}-{counter}'
|
||||
counter += 1
|
||||
self.slug = slug
|
||||
|
||||
# 如果是新对象或图片被更新,生成缩略图
|
||||
if self.pk is None or 'image' in kwargs.get('update_fields', []):
|
||||
super().save(*args, **kwargs)
|
||||
self.generate_thumbnail()
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def generate_thumbnail(self):
|
||||
"""生成缩略图"""
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from django.core.files.base import ContentFile
|
||||
import os
|
||||
|
||||
if not self.image:
|
||||
return
|
||||
|
||||
# 打开原图
|
||||
img = Image.open(self.image)
|
||||
|
||||
# 计算目标像素总量:约90万像素(如1280x720)
|
||||
target_pixels = 900000
|
||||
|
||||
# 获取原图尺寸
|
||||
width, height = img.size
|
||||
current_pixels = width * height
|
||||
|
||||
# 计算缩放比例,使像素总量接近90万(但限制最大边不超过1600px)
|
||||
max_dimension = 1600
|
||||
|
||||
if current_pixels <= 0:
|
||||
# 如果图片尺寸无效,使用原尺寸但限制最大边
|
||||
if width > max_dimension or height > max_dimension:
|
||||
scale = max_dimension / max(width, height)
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
# 首先计算缩放至目标像素的比例
|
||||
scale_to_target = (target_pixels / current_pixels) ** 0.5
|
||||
|
||||
# 然后检查缩放后是否超过最大边限制
|
||||
new_width_temp = int(width * scale_to_target)
|
||||
new_height_temp = int(height * scale_to_target)
|
||||
|
||||
# 如果缩放后的尺寸超过最大边限制,使用最大边限制的比例
|
||||
if new_width_temp > max_dimension or new_height_temp > max_dimension:
|
||||
scale_to_max = max_dimension / max(new_width_temp, new_height_temp)
|
||||
scale = scale_to_target * scale_to_max
|
||||
else:
|
||||
scale = scale_to_target
|
||||
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# 保存到内存
|
||||
thumb_io = BytesIO()
|
||||
|
||||
# 保持原格式,如果是JPEG则优化
|
||||
# 如果img.format为空,根据文件扩展名判断格式
|
||||
format_to_use = img.format
|
||||
if not format_to_use:
|
||||
# 根据文件扩展名判断格式
|
||||
filename = os.path.basename(self.image.name)
|
||||
name, ext = os.path.splitext(filename)
|
||||
ext = ext.lower()
|
||||
if ext in ['.jpg', '.jpeg', '.jpe', '.jfif']:
|
||||
format_to_use = 'JPEG'
|
||||
elif ext in ['.png']:
|
||||
format_to_use = 'PNG'
|
||||
elif ext in ['.gif']:
|
||||
format_to_use = 'GIF'
|
||||
elif ext in ['.webp']:
|
||||
format_to_use = 'WEBP'
|
||||
else:
|
||||
# 默认使用JPEG格式
|
||||
format_to_use = 'JPEG'
|
||||
|
||||
if format_to_use == 'JPEG':
|
||||
img.save(thumb_io, format='JPEG', quality=85, optimize=True)
|
||||
else:
|
||||
img.save(thumb_io, format=format_to_use)
|
||||
|
||||
# 生成缩略图文件名
|
||||
filename = os.path.basename(self.image.name)
|
||||
name, ext = os.path.splitext(filename)
|
||||
thumb_filename = f'{name}_thumb{ext}'
|
||||
|
||||
# 保存缩略图
|
||||
self.thumbnail.save(
|
||||
thumb_filename,
|
||||
ContentFile(thumb_io.getvalue()),
|
||||
save=False
|
||||
)
|
||||
|
||||
super().save(update_fields=['thumbnail'])
|
||||
|
||||
|
||||
def increment_view_count(self):
|
||||
"""增加浏览次数"""
|
||||
self.view_count += 1
|
||||
self.save(update_fields=['view_count'])
|
||||
|
||||
def get_dynamic_grid_class(self):
|
||||
"""根据图片宽高比动态返回网格CSS类"""
|
||||
try:
|
||||
if not self.thumbnail:
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
# 尝试从缩略图获取尺寸
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
thumb_path = self.thumbnail.path
|
||||
if not os.path.exists(thumb_path):
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
with Image.open(thumb_path) as img:
|
||||
width, height = img.size
|
||||
aspect_ratio = width / height
|
||||
|
||||
# 根据宽高比返回不同的网格类
|
||||
if aspect_ratio > 1.5: # 很宽的图片
|
||||
return 'col-span-2 row-span-1' # 占2列宽
|
||||
elif aspect_ratio < 0.67: # 很高的图片
|
||||
return 'col-span-1 row-span-2' # 占2行高
|
||||
elif aspect_ratio > 1.2: # 中等宽度
|
||||
return 'col-span-2 row-span-1' # 占2列宽
|
||||
elif aspect_ratio < 0.83: # 中等高度
|
||||
return 'col-span-1 row-span-2' # 占2行高
|
||||
else: # 接近正方形
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
except Exception as e:
|
||||
# 发生错误时返回默认
|
||||
print(f"Error getting grid class for artwork {self.id}: {e}")
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
|
||||
class About(models.Model):
|
||||
"""关于页面模型"""
|
||||
title = models.CharField('标题', max_length=200)
|
||||
content = models.TextField('内容')
|
||||
image = models.ImageField('图片', upload_to='about/', blank=True, null=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '关于页面'
|
||||
verbose_name_plural = '关于页面'
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 确保只有一个关于页面
|
||||
if not self.pk and About.objects.exists():
|
||||
# 如果已经存在关于页面,更新它而不是创建新的
|
||||
existing = About.objects.first()
|
||||
existing.title = self.title
|
||||
existing.content = self.content
|
||||
if self.image:
|
||||
existing.image = self.image
|
||||
existing.save()
|
||||
return existing
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
"""评论模型"""
|
||||
artwork = models.ForeignKey(Artwork, on_delete=models.CASCADE, verbose_name='作品')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='用户')
|
||||
text = models.TextField('评论内容', blank=True)
|
||||
image = models.ImageField('评论图片', upload_to='comments/%Y/%m/%d/', blank=True, null=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
is_active = models.BooleanField('是否有效', default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '评论'
|
||||
verbose_name_plural = '评论'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} 对 {self.artwork.title} 的评论"
|
||||
273
gallery/static/gallery/css/custom.css
Executable file
273
gallery/static/gallery/css/custom.css
Executable file
@@ -0,0 +1,273 @@
|
||||
/* YITAO-REN GALLERY 自定义样式 */
|
||||
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 页面加载动画 */
|
||||
body:not(.loaded) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
body.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(45deg, #3b82f6, #8b5cf6);
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(45deg, #2563eb, #7c3aed);
|
||||
}
|
||||
|
||||
/* 图片懒加载过渡效果 */
|
||||
.lazy-image {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.lazy-image.lazy-loaded {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 网格项悬停效果增强 */
|
||||
.grid-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.grid-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.1) 50%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-item:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.grid-item:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 导航链接下划线动画增强 */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||
transform: translateX(-50%);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 按钮样式增强 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 图片缩放效果 */
|
||||
.image-zoom-container {
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.image-zoom {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.image-zoom:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 文字渐变效果 */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(59, 130, 246, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #3b82f6;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面过渡动画 */
|
||||
.page-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.page-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
|
||||
.page-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.page-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.grid-item:hover {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
a {
|
||||
color: black !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式支持 */
|
||||
@media (prefers-contrast: high) {
|
||||
.card {
|
||||
border: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
275
gallery/static/gallery/js/lazy-load.js
Executable file
275
gallery/static/gallery/js/lazy-load.js
Executable file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* YITAO-REN GALLERY 图片懒加载脚本
|
||||
* 优化图片加载性能,提升用户体验
|
||||
*/
|
||||
|
||||
class LazyLoader {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
root: null,
|
||||
rootMargin: '50px 0px',
|
||||
threshold: 0.1,
|
||||
placeholder: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect width="100" height="100" fill="%231e293b"/%3E%3C/svg%3E',
|
||||
loadingClass: 'lazy-loading',
|
||||
loadedClass: 'lazy-loaded',
|
||||
errorClass: 'lazy-error',
|
||||
...options
|
||||
};
|
||||
|
||||
this.observer = null;
|
||||
this.images = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建 IntersectionObserver 实例
|
||||
this.observer = new IntersectionObserver(
|
||||
this.handleIntersection.bind(this),
|
||||
this.options
|
||||
);
|
||||
|
||||
// 查找所有需要懒加载的图片
|
||||
this.findImages();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
findImages() {
|
||||
const selectors = [
|
||||
'img[data-src]',
|
||||
'img[data-srcset]',
|
||||
'source[data-srcset]',
|
||||
'iframe[data-src]'
|
||||
].join(',');
|
||||
|
||||
document.querySelectorAll(selectors).forEach(element => {
|
||||
this.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
observe(element) {
|
||||
// 设置占位符
|
||||
if (element.tagName === 'IMG' && !element.src) {
|
||||
element.src = this.options.placeholder;
|
||||
element.classList.add(this.options.loadingClass);
|
||||
}
|
||||
|
||||
// 添加到观察列表
|
||||
this.images.set(element, {
|
||||
src: element.dataset.src,
|
||||
srcset: element.dataset.srcset,
|
||||
loaded: false
|
||||
});
|
||||
|
||||
// 开始观察
|
||||
this.observer.observe(element);
|
||||
}
|
||||
|
||||
handleIntersection(entries) {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
this.loadImage(entry.target);
|
||||
this.observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadImage(element) {
|
||||
const imageData = this.images.get(element);
|
||||
if (!imageData || imageData.loaded) return;
|
||||
|
||||
imageData.loaded = true;
|
||||
|
||||
// 创建新的 Image 对象预加载
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
this.setImageSource(element, imageData);
|
||||
this.handleLoadSuccess(element);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
this.handleLoadError(element);
|
||||
};
|
||||
|
||||
// 开始加载
|
||||
img.src = imageData.src || imageData.srcset || '';
|
||||
}
|
||||
|
||||
setImageSource(element, imageData) {
|
||||
if (imageData.src && element.tagName === 'IMG') {
|
||||
element.src = imageData.src;
|
||||
}
|
||||
|
||||
if (imageData.srcset) {
|
||||
if (element.tagName === 'IMG') {
|
||||
element.srcset = imageData.srcset;
|
||||
} else if (element.tagName === 'SOURCE') {
|
||||
element.srcset = imageData.srcset;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除 data 属性以节省内存
|
||||
delete element.dataset.src;
|
||||
delete element.dataset.srcset;
|
||||
}
|
||||
|
||||
handleLoadSuccess(element) {
|
||||
element.classList.remove(this.options.loadingClass);
|
||||
element.classList.add(this.options.loadedClass);
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('lazyload:loaded', {
|
||||
detail: { element }
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
|
||||
// 添加淡入动画
|
||||
this.animateFadeIn(element);
|
||||
}
|
||||
|
||||
handleLoadError(element) {
|
||||
element.classList.remove(this.options.loadingClass);
|
||||
element.classList.add(this.options.errorClass);
|
||||
|
||||
// 设置错误占位符
|
||||
if (element.tagName === 'IMG') {
|
||||
element.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect width="100" height="100" fill="%231e293b"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%239ca3af" font-size="8"%3E加载失败%3C/text%3E%3C/svg%3E';
|
||||
}
|
||||
|
||||
// 触发自定义事件
|
||||
const event = new CustomEvent('lazyload:error', {
|
||||
detail: { element }
|
||||
});
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
animateFadeIn(element) {
|
||||
element.style.opacity = '0';
|
||||
element.style.transition = 'opacity 0.5s ease';
|
||||
|
||||
// 使用 requestAnimationFrame 确保样式已应用
|
||||
requestAnimationFrame(() => {
|
||||
element.style.opacity = '1';
|
||||
|
||||
// 动画完成后清理样式
|
||||
setTimeout(() => {
|
||||
element.style.transition = '';
|
||||
element.style.opacity = '';
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// 监听动态添加的图片
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === 1) { // Element node
|
||||
if (node.matches && node.matches('img[data-src], img[data-srcset], source[data-srcset], iframe[data-src]')) {
|
||||
this.observe(node);
|
||||
}
|
||||
|
||||
// 检查子元素
|
||||
node.querySelectorAll?.('img[data-src], img[data-srcset], source[data-srcset], iframe[data-src]').forEach(element => {
|
||||
this.observe(element);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// 窗口 resize 时重新检查可见性
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
this.checkVisibleImages();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
// 页面可见性变化时重新检查
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
this.checkVisibleImages();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkVisibleImages() {
|
||||
// 重新检查所有未加载的图片
|
||||
this.images.forEach((data, element) => {
|
||||
if (!data.loaded) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const isVisible = (
|
||||
rect.top <= window.innerHeight + 100 &&
|
||||
rect.bottom >= -100 &&
|
||||
rect.left <= window.innerWidth + 100 &&
|
||||
rect.right >= -100
|
||||
);
|
||||
|
||||
if (isVisible) {
|
||||
this.loadImage(element);
|
||||
this.observer.unobserve(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
|
||||
this.images.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 自动初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.lazyLoader = new LazyLoader();
|
||||
|
||||
// 暴露全局方法
|
||||
window.lazyLoadImages = () => {
|
||||
window.lazyLoader?.checkVisibleImages();
|
||||
};
|
||||
|
||||
// 预加载关键图片
|
||||
const criticalImages = document.querySelectorAll('.critical-image[data-src]');
|
||||
criticalImages.forEach(img => {
|
||||
const src = img.dataset.src;
|
||||
if (src) {
|
||||
const preloadLink = document.createElement('link');
|
||||
preloadLink.rel = 'preload';
|
||||
preloadLink.as = 'image';
|
||||
preloadLink.href = src;
|
||||
document.head.appendChild(preloadLink);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 导出供模块化使用
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = LazyLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
*
|
||||
* 1. 基本用法:
|
||||
* <img data-src="/path/to/image.jpg" class="lazy-image">
|
||||
*
|
||||
* 2. 响应式图片:
|
||||
* <img data-src="/small.jpg" data-srcset="/small.jpg 480w, /medium.jpg 768w, /large.jpg 1200w" sizes="(max-width: 600px) 480px, (max-width: 1200px) 768px, 1200px">
|
||||
*
|
||||
* 3. 背景图片懒加载:
|
||||
* <div data-bg="/path/to/image.jpg" class="lazy-bg"></div>
|
||||
*
|
||||
* 4. 手动触发加载:
|
||||
* window.lazyLoadImages();
|
||||
*/
|
||||
BIN
gallery/templates/gallery.zip
Normal file
BIN
gallery/templates/gallery.zip
Normal file
Binary file not shown.
217
gallery/templates/gallery/about.html
Executable file
217
gallery/templates/gallery/about.html
Executable file
@@ -0,0 +1,217 @@
|
||||
{% extends 'gallery/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 关于页面头部 -->
|
||||
<div class="text-center mb-12 animate-fade-in">
|
||||
<h1 class="text-4xl md:text-5xl font-serif font-bold mb-6">关于画廊</h1>
|
||||
<p class="text-gray-400 max-w-3xl mx-auto text-lg">
|
||||
Yitao-Ren Gallery 致力于展示现代摄影艺术的独特魅力,<br>
|
||||
每一幅作品都是摄影师对世界的独特诠释。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 关于内容 -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
{% if about %}
|
||||
<!-- 如果有关于页面内容 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-16 animate-slide-up">
|
||||
<!-- 图片 -->
|
||||
{% if about.image %}
|
||||
<div class="relative overflow-hidden rounded-2xl bg-dark-card">
|
||||
<img
|
||||
src="{{ about.image.url }}"
|
||||
alt="{{ about.title }}"
|
||||
class="w-full h-auto object-cover rounded-2xl"
|
||||
loading="lazy"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="relative overflow-hidden rounded-2xl bg-dark-card flex items-center justify-center h-64">
|
||||
<div class="text-center">
|
||||
<svg class="w-16 h-16 mx-auto text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
|
||||
</svg>
|
||||
<p class="text-gray-500">画廊图片</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-3xl font-serif font-bold">{{ about.title }}</h2>
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div class="text-gray-300 leading-relaxed whitespace-pre-line">
|
||||
{{ about.content|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 默认关于内容 -->
|
||||
<div class="bg-dark-card rounded-2xl p-8 md:p-12 mb-16 animate-slide-up">
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<h2 class="text-3xl font-serif font-bold mb-6">YITAO-REN GALLERY</h2>
|
||||
|
||||
<div class="space-y-6 text-gray-300">
|
||||
<p>
|
||||
成立于2026年,Yitao-Ren Gallery 是一个专注于现代摄影艺术展示的在线平台。
|
||||
我们致力于发现和推广具有独特视角和艺术价值的摄影作品,为艺术家和艺术爱好者
|
||||
搭建一个交流与展示的空间。
|
||||
</p>
|
||||
|
||||
<p>
|
||||
画廊的名字来源于创始人对于摄影艺术的热爱与追求。"Yitao"代表着艺术的独特道路,
|
||||
"Ren"象征着人文关怀。我们相信,每一幅摄影作品都是摄影师与世界对话的方式,
|
||||
是瞬间与永恒的完美结合。
|
||||
</p>
|
||||
|
||||
<h3 class="text-xl font-semibold mt-8 mb-4 text-gray-200">我们的使命</h3>
|
||||
<ul class="list-disc pl-6 space-y-2">
|
||||
<li>展示高质量的现代摄影艺术作品</li>
|
||||
<li>支持新兴摄影艺术家的发展</li>
|
||||
<li>促进摄影艺术的交流与传播</li>
|
||||
<li>为艺术爱好者提供优质的观赏体验</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-xl font-semibold mt-8 mb-4 text-gray-200">画廊特色</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-gray-900/50 rounded-lg p-4">
|
||||
<div class="text-blue-400 font-semibold mb-2">专业策展</div>
|
||||
<p class="text-sm text-gray-400">每幅作品都经过精心挑选和策展</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4">
|
||||
<div class="text-blue-400 font-semibold mb-2">高清展示</div>
|
||||
<p class="text-sm text-gray-400">提供高质量的作品图片展示</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4">
|
||||
<div class="text-blue-400 font-semibold mb-2">艺术家支持</div>
|
||||
<p class="text-sm text-gray-400">为艺术家提供展示和推广平台</p>
|
||||
</div>
|
||||
<div class="bg-gray-900/50 rounded-lg p-4">
|
||||
<div class="text-blue-400 font-semibold mb-2">社区交流</div>
|
||||
<p class="text-sm text-gray-400">促进艺术爱好者之间的交流</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 团队介绍 -->
|
||||
<div class="mb-16">
|
||||
<h2 class="text-3xl font-serif font-bold text-center mb-12">我们的团队</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="text-center">
|
||||
<div class="w-32 h-32 mx-auto mb-4 rounded-full bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center">
|
||||
<span class="text-4xl font-bold text-white">YR</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Yitao Ren</h3>
|
||||
<p class="text-gray-400">创始人 & 策展人</p>
|
||||
<p class="text-sm text-gray-500 mt-3">拥有10年摄影艺术策展经验</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-32 h-32 mx-auto mb-4 rounded-full bg-gradient-to-br from-green-600 to-blue-600 flex items-center justify-center">
|
||||
<span class="text-4xl font-bold text-white">LT</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Li Tao</h3>
|
||||
<p class="text-gray-400">技术总监</p>
|
||||
<p class="text-sm text-gray-500 mt-3">负责画廊平台的技术开发与维护</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="w-32 h-32 mx-auto mb-4 rounded-full bg-gradient-to-br from-purple-600 to-pink-600 flex items-center justify-center">
|
||||
<span class="text-4xl font-bold text-white">AW</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2">Art Wang</h3>
|
||||
<p class="text-gray-400">艺术顾问</p>
|
||||
<p class="text-sm text-gray-500 mt-3">资深艺术评论家,摄影艺术专家</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<div class="bg-dark-card rounded-2xl p-8 md:p-12">
|
||||
<h2 class="text-3xl font-serif font-bold text-center mb-8">联系我们</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-3 text-gray-200">画廊地址</h3>
|
||||
<p class="text-gray-400">
|
||||
北京市朝阳区艺术大道123号<br>
|
||||
艺术创意园区A座3层
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-3 text-gray-200">开放时间</h3>
|
||||
<p class="text-gray-400">
|
||||
周二至周日:10:00 - 18:00<br>
|
||||
周一闭馆
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-3 text-gray-200">联系方式</h3>
|
||||
<p class="text-gray-400">
|
||||
电话:+86 10 1234 5678<br>
|
||||
邮箱:info@yitaoren-gallery.com<br>
|
||||
微信:YitaoRenGallery
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-3 text-gray-200">关注我们</h3>
|
||||
<div class="flex space-x-4">
|
||||
<a href="#" class="text-gray-400 hover:text-blue-400 transition-colors">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="text-gray-400 hover:text-blue-400 transition-colors">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="text-gray-400 hover:text-blue-400 transition-colors">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M22.675 0h-21.35c-.732 0-1.325.593-1.325 1.325v21.351c0 .731.593 1.324 1.325 1.324h11.495v-9.294h-3.128v-3.622h3.128v-2.671c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12v9.293h6.116c.73 0 1.323-.593 1.323-1.325v-21.35c0-.732-.593-1.325-1.325-1.325z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 平滑滚动到联系信息
|
||||
document.querySelectorAll('a[href="#contact"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const contactSection = document.querySelector('.bg-dark-card');
|
||||
if (contactSection) {
|
||||
contactSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 团队卡片悬停效果
|
||||
document.querySelectorAll('.text-center').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.transform = 'translateY(-8px)';
|
||||
this.style.transition = 'transform 0.3s ease';
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.transform = 'translateY(0)';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
63
gallery/templates/gallery/auth/login.html
Normal file
63
gallery/templates/gallery/auth/login.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "gallery/base.html" %}
|
||||
|
||||
{% block title %}登录 - {{ site_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-white">登录账户</h2>
|
||||
<p class="mt-2 text-gray-400">请使用管理员提供的账户登录</p>
|
||||
</div>
|
||||
|
||||
<form method="post" class="mt-8 space-y-6 bg-gray-900 border border-gray-800 rounded-lg p-8">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if messages %}
|
||||
<div class="space-y-2">
|
||||
{% for message in messages %}
|
||||
<div class="p-3 rounded-lg {% if message.tags == 'error' %}bg-red-900/30 border border-red-800 text-red-400{% else %}bg-blue-900/30 border border-blue-800 text-blue-400{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-gray-300 text-sm mb-2">用户名</label>
|
||||
<input type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="请输入用户名">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-gray-300 text-sm mb-2">密码</label>
|
||||
<input type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="请输入密码">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition duration-200">
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-gray-500 text-sm">
|
||||
需要账户?请联系管理员创建
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
gallery/templates/gallery/auth/logout.html
Normal file
26
gallery/templates/gallery/auth/logout.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "gallery/base.html" %}
|
||||
|
||||
{% block title %}登出 - {{ site_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4">
|
||||
<div class="max-w-md w-full space-y-8 text-center">
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-8">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">确认登出</h2>
|
||||
<p class="text-gray-400 mb-8">您确定要退出登录吗?</p>
|
||||
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
class="w-full py-3 px-4 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition duration-200">
|
||||
确认登出
|
||||
</button>
|
||||
<a href="{% url 'index' %}"
|
||||
class="block py-3 px-4 bg-gray-800 hover:bg-gray-700 text-white font-medium rounded-lg transition duration-200">
|
||||
取消
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
284
gallery/templates/gallery/base.html
Executable file
284
gallery/templates/gallery/base.html
Executable file
@@ -0,0 +1,284 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ page_title|default:site_name }}{% endblock %}</title>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="Yitao-Ren Gallery - 现代摄影作品展示网站">
|
||||
<meta name="keywords" content="摄影,画廊,艺术作品,现代艺术">
|
||||
<meta name="author" content="Yitao-Ren Gallery">
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="{% block og_title %}{{ page_title|default:site_name }}{% endblock %}">
|
||||
<meta property="og:description" content="Yitao-Ren Gallery - 现代摄影作品展示网站">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
|
||||
<!-- Twitter Card Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{% block twitter_title %}{{ page_title|default:site_name }}{% endblock %}">
|
||||
<meta name="twitter:description" content="Yitao-Ren Gallery - 现代摄影作品展示网站">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&family=Playfair+Display&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://gallery.lizexua.com/static/dist/styles.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3b82f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
/* 图片懒加载样式 */
|
||||
.lazy-image {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
.lazy-image.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 页面过渡动画 */
|
||||
.page-transition {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 网格项悬停效果 */
|
||||
.grid-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.grid-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* 导航栏下划线动画 */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
}
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: #3b82f6;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.nav-link:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-dark text-gray-100 font-sans min-h-screen flex flex-col">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="fixed top-0 left-0 right-0 bg-black/80 backdrop-blur-sm z-50 border-b border-gray-800">
|
||||
<div class="container mx-auto px-4 py-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a href="{% url 'index' %}" class="text-xl font-serif font-bold uppercase tracking-wider hover:text-blue-400 transition-colors">
|
||||
YITAO-REN GALLERY
|
||||
</a>
|
||||
|
||||
<!-- 导航链接 -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="{% url 'index' %}" class="nav-link text-gray-300 hover:text-blue-400 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
|
||||
<!-- 分类下拉菜单 -->
|
||||
{% if categories %}
|
||||
<div class="relative group">
|
||||
<button class="nav-link text-gray-300 hover:text-blue-400 transition-colors flex items-center">
|
||||
Categories
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="absolute top-full left-0 mt-2 w-48 bg-dark-card rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 border border-gray-800">
|
||||
{% for category in categories %}
|
||||
<a href="{% url 'category' category.slug %}" class="block px-4 py-3 text-gray-300 hover:bg-gray-800 hover:text-blue-400 transition-colors border-b border-gray-800 last:border-b-0">
|
||||
{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'about' %}" class="nav-link text-gray-300 hover:text-blue-400 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 用户状态(桌面端) -->
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
{% if user.is_authenticated %}
|
||||
<!-- 已登录状态 -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-gray-800 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-medium">{{ user.username|first|upper }}</span>
|
||||
</div>
|
||||
<span class="text-gray-300 text-sm">{{ user.username }}</span>
|
||||
<a href="{% url 'logout' %}"
|
||||
class="px-3 py-1.5 text-sm bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition duration-200">
|
||||
登出
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 未登录状态 -->
|
||||
<a href="{% url 'login' %}"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition duration-200">
|
||||
登录
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<button id="mobile-menu-button" class="md:hidden text-gray-300 hover:text-blue-400">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div id="mobile-menu" class="md:hidden mt-3 hidden">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<a href="{% url 'index' %}" class="text-gray-300 hover:text-blue-400 transition-colors py-2">
|
||||
Gallery
|
||||
</a>
|
||||
|
||||
{% if categories %}
|
||||
<div class="border-t border-gray-800 pt-2">
|
||||
<div class="text-gray-400 text-sm mb-2">Categories</div>
|
||||
{% for category in categories %}
|
||||
<a href="{% url 'category' category.slug %}" class="block text-gray-300 hover:text-blue-400 transition-colors py-1 pl-4">
|
||||
{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'about' %}" class="text-gray-300 hover:text-blue-400 transition-colors py-2 border-t border-gray-800">
|
||||
About
|
||||
</a>
|
||||
|
||||
<!-- 用户状态(移动端) -->
|
||||
<div class="border-t border-gray-800 pt-4">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 bg-gray-800 rounded-full flex items-center justify-center mr-3">
|
||||
<span class="text-white text-sm font-medium">{{ user.username|first|upper }}</span>
|
||||
</div>
|
||||
<span class="text-gray-300 text-sm">{{ user.username }}</span>
|
||||
</div>
|
||||
<a href="{% url 'logout' %}"
|
||||
class="px-3 py-1.5 text-sm bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition duration-200">
|
||||
登出
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{% url 'login' %}"
|
||||
class="block w-full text-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition duration-200">
|
||||
登录
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="flex-grow pt-16 page-transition">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-black/50 border-t border-gray-800 mt-12">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="text-center">
|
||||
<div class="text-gray-400 text-sm mb-2">
|
||||
{{ copyright_text }}
|
||||
</div>
|
||||
<div class="text-gray-500 text-xs">
|
||||
All artworks are copyrighted by their respective owners.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
// 移动端菜单切换
|
||||
document.getElementById('mobile-menu-button').addEventListener('click', function() {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
// 图片懒加载
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const lazyImages = document.querySelectorAll('.lazy-image');
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.add('loaded');
|
||||
observer.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lazyImages.forEach(img => imageObserver.observe(img));
|
||||
});
|
||||
|
||||
// 平滑滚动
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const targetId = this.getAttribute('href');
|
||||
if (targetId === '#') return;
|
||||
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载动画
|
||||
window.addEventListener('load', function() {
|
||||
document.body.classList.add('loaded');
|
||||
});
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
90
gallery/templates/gallery/category.html
Executable file
90
gallery/templates/gallery/category.html
Executable file
@@ -0,0 +1,90 @@
|
||||
{% extends 'gallery/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 面包屑导航 -->
|
||||
<nav class="mb-8">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-400">
|
||||
<li>
|
||||
<a href="{% url 'index' %}" class="hover:text-blue-400 transition-colors">Gallery</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="mx-2">/</span>
|
||||
</li>
|
||||
<li class="text-gray-300">{{ category.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 分类标题 -->
|
||||
<div class="mb-12 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-serif font-bold mb-4">{{ category.name }}</h1>
|
||||
<p class="text-gray-400 max-w-2xl mx-auto">
|
||||
{{ artworks.count }} 个作品
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 作品网格 -->
|
||||
{% if artworks %}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{% for artwork in artworks %}
|
||||
<a href="{% url 'artwork_detail' artwork.slug %}" class="group">
|
||||
<div class="relative overflow-hidden rounded-xl bg-dark-card transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl">
|
||||
<!-- 图片 -->
|
||||
<div class="aspect-square overflow-hidden">
|
||||
<img
|
||||
data-src="{{ artwork.thumbnail.url|default:artwork.image.url }}"
|
||||
alt="{{ artwork.title }}"
|
||||
class="lazy-image w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- 作品信息 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="bg-black/80 backdrop-blur-sm rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-2 truncate">{{ artwork.title }}</h3>
|
||||
|
||||
{% if artwork.description %}
|
||||
<p class="text-gray-300 text-sm line-clamp-2 mb-3">
|
||||
{{ artwork.description|truncatechars:80 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||
<span>{{ artwork.created_at|date:"Y年m月d日" }}</span>
|
||||
{% if artwork.view_count %}
|
||||
<span>{{ artwork.view_count }} 次浏览</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 空状态 -->
|
||||
<div class="text-center py-20">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gray-800/50 mb-6">
|
||||
<svg class="w-12 h-12 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-300 mb-2">暂无作品</h3>
|
||||
<p class="text-gray-500 max-w-md mx-auto mb-8">
|
||||
该分类下暂时没有作品,请稍后再来查看。
|
||||
</p>
|
||||
<a href="{% url 'index' %}" class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回 Gallery
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
gallery/templates/gallery/comments/comment_form.html
Normal file
42
gallery/templates/gallery/comments/comment_form.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<form method="post" action="{% url 'add_comment' artwork.slug %}" enctype="multipart/form-data" class="mb-8">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-white mb-4">发表评论</h3>
|
||||
|
||||
<!-- 文本输入 -->
|
||||
<div class="mb-4">
|
||||
{{ comment_form.text }}
|
||||
{% if comment_form.text.errors %}
|
||||
<div class="mt-2 text-red-400 text-sm">{{ comment_form.text.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-400 text-sm mb-2">上传图片(可选)</label>
|
||||
{{ comment_form.image }}
|
||||
<p class="text-gray-500 text-xs mt-2">支持 JPG、PNG、GIF、WEBP 格式,最大 5MB</p>
|
||||
{% if comment_form.image.errors %}
|
||||
<div class="mt-2 text-red-400 text-sm">{{ comment_form.image.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 表单错误 -->
|
||||
{% if comment_form.non_field_errors %}
|
||||
<div class="mb-4 p-3 bg-red-900/30 border border-red-800 rounded-lg">
|
||||
{% for error in comment_form.non_field_errors %}
|
||||
<p class="text-red-400 text-sm">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition duration-200">
|
||||
发布评论
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
35
gallery/templates/gallery/comments/comment_item.html
Normal file
35
gallery/templates/gallery/comments/comment_item.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-6 mb-4">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center mr-3">
|
||||
<span class="text-white font-medium">{{ comment.user.username|first|upper }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">{{ comment.user.username }}</p>
|
||||
<p class="text-gray-500 text-sm">{{ comment.created_at|date:"Y年m月d日 H:i" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if comment.user == user %}
|
||||
<form method="post" action="{% url 'delete_comment' comment.pk %}" class="inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit"
|
||||
onclick="return confirm('确定要删除这条评论吗?')"
|
||||
class="text-red-400 hover:text-red-300 text-sm">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if comment.text %}
|
||||
<div class="text-white mb-4 leading-relaxed">{{ comment.text|linebreaks }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if comment.image %}
|
||||
<div class="mt-4">
|
||||
<img src="{{ comment.image.url }}"
|
||||
alt="评论图片"
|
||||
class="max-w-full h-auto rounded-lg border border-gray-800 max-h-96 object-contain">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
32
gallery/templates/gallery/comments/comment_list.html
Normal file
32
gallery/templates/gallery/comments/comment_list.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="space-y-4">
|
||||
{% for comment in comments %}
|
||||
{% include "gallery/comments/comment_item.html" %}
|
||||
{% empty %}
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-8 text-center">
|
||||
<p class="text-gray-400">暂无评论,成为第一个评论者吧!</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 分页导航 -->
|
||||
{% if comments.paginator.num_pages > 1 %}
|
||||
<div class="flex justify-center items-center space-x-2 mt-8">
|
||||
{% if comments.has_previous %}
|
||||
<a href="?page={{ comments.previous_page_number }}#comments"
|
||||
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-white rounded-lg">
|
||||
上一页
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="text-gray-400">
|
||||
第 {{ comments.number }} / {{ comments.paginator.num_pages }} 页
|
||||
</span>
|
||||
|
||||
{% if comments.has_next %}
|
||||
<a href="?page={{ comments.next_page_number }}#comments"
|
||||
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-white rounded-lg">
|
||||
下一页
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
337
gallery/templates/gallery/detail.html
Executable file
337
gallery/templates/gallery/detail.html
Executable file
@@ -0,0 +1,337 @@
|
||||
{% extends 'gallery/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 面包屑导航 -->
|
||||
<nav class="mb-8">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-400">
|
||||
<li>
|
||||
<a href="{% url 'index' %}" class="hover:text-blue-400 transition-colors">Gallery</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="mx-2">/</span>
|
||||
</li>
|
||||
{% if artwork.category %}
|
||||
<li>
|
||||
<a href="{% url 'category' artwork.category.slug %}" class="hover:text-blue-400 transition-colors">
|
||||
{{ artwork.category.name }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="mx-2">/</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="text-gray-300">{{ artwork.title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<div class="mb-6">
|
||||
<a href="{% url 'index' %}" class="inline-flex items-center text-blue-400 hover:text-blue-300 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回 Gallery
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 作品详情内容 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 animate-fade-in">
|
||||
<!-- 左侧:图片展示 -->
|
||||
<div class="space-y-6">
|
||||
<!-- 主图 -->
|
||||
<div class="relative overflow-hidden rounded-xl bg-dark-card">
|
||||
<img
|
||||
src="{{ artwork.image.url }}"
|
||||
alt="{{ artwork.title }}"
|
||||
class="w-full h-auto max-w-full block"
|
||||
loading="eager"
|
||||
>
|
||||
|
||||
<!-- 图片信息 -->
|
||||
<div class="absolute bottom-4 left-4 right-4 flex justify-end items-center">
|
||||
<div class="text-xs text-gray-400 bg-black/70 px-3 py-1 rounded-full backdrop-blur-sm">
|
||||
{{ artwork.created_at|date:"Y年m月d日" }}
|
||||
{% if artwork.view_count %}
|
||||
· {{ artwork.view_count }} 次浏览
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缩略图导航(如果有多个图片) -->
|
||||
{% if prev_artwork or next_artwork %}
|
||||
<div class="flex justify-between items-center pt-4 border-t border-gray-800">
|
||||
{% if prev_artwork %}
|
||||
<a href="{% url 'artwork_detail' prev_artwork.slug %}" class="group flex items-center text-gray-400 hover:text-blue-400 transition-colors">
|
||||
<svg class="w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-gray-500">上一幅</div>
|
||||
<div class="text-sm">{{ prev_artwork.title|truncatechars:20 }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if next_artwork %}
|
||||
<a href="{% url 'artwork_detail' next_artwork.slug %}" class="group flex items-center text-gray-400 hover:text-blue-400 transition-colors">
|
||||
<div class="text-left mr-2">
|
||||
<div class="text-xs text-gray-500">下一幅</div>
|
||||
<div class="text-sm">{{ next_artwork.title|truncatechars:20 }}</div>
|
||||
</div>
|
||||
<svg class="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
{% else %}
|
||||
<div></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 右侧:作品信息 -->
|
||||
<div class="space-y-8">
|
||||
<!-- 标题和分类 -->
|
||||
<div>
|
||||
<h1 class="text-4xl md:text-5xl font-serif font-bold mb-4">{{ artwork.title }}</h1>
|
||||
|
||||
{% if artwork.category %}
|
||||
<div class="inline-flex items-center mb-6">
|
||||
<a href="{% url 'category' artwork.category.slug %}" class="px-4 py-2 bg-blue-600/20 text-blue-400 rounded-full hover:bg-blue-600/30 transition-colors">
|
||||
{{ artwork.category.name }}
|
||||
</a>
|
||||
<span class="mx-4 text-gray-500">•</span>
|
||||
<span class="text-gray-400">{{ artwork.created_at|date:"Y年m月d日" }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 作品描述 -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-gray-800">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-300">作品描述</h3>
|
||||
<div class="text-gray-300 leading-relaxed whitespace-pre-line">
|
||||
{{ artwork.description|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作品元数据 -->
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-gray-800">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-300">作品信息</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">作品编号</div>
|
||||
<div class="text-gray-300 font-mono">ART-{{ artwork.id|stringformat:"04d" }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">上传时间</div>
|
||||
<div class="text-gray-300">{{ artwork.created_at|date:"Y-m-d H:i" }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">最后更新</div>
|
||||
<div class="text-gray-300">{{ artwork.updated_at|date:"Y-m-d H:i" }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">浏览次数</div>
|
||||
<div class="text-gray-300">{{ artwork.view_count }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">排序序号</div>
|
||||
<div class="text-gray-300">{{ artwork.order }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享和操作 -->
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<button onclick="shareArtwork()" class="flex-1 md:flex-none px-6 py-3 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition-colors flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path>
|
||||
</svg>
|
||||
分享作品
|
||||
</button>
|
||||
|
||||
<a href="{{ artwork.image.url }}" download class="flex-1 md:flex-none px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
下载原图
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 相关作品(同分类) -->
|
||||
{% if related_artworks %}
|
||||
<div class="mt-16 pt-12 border-t border-gray-800">
|
||||
<h2 class="text-2xl font-semibold mb-6">同分类作品</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{% for related in related_artworks %}
|
||||
<a href="{% url 'artwork_detail' related.slug %}" class="group">
|
||||
<div class="relative overflow-hidden rounded-lg bg-dark-card">
|
||||
<img
|
||||
data-src="{{ related.thumbnail.url|default:related.image.url }}"
|
||||
alt="{{ related.title }}"
|
||||
class="lazy-image w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-all duration-300"></div>
|
||||
<div class="p-3">
|
||||
<h3 class="text-sm font-medium text-gray-300 group-hover:text-blue-400 transition-colors truncate">
|
||||
{{ related.title }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 评论区域 -->
|
||||
<section class="mt-16 pt-12 border-t border-gray-800" id="comments">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-2">
|
||||
评论 <span class="text-gray-400">({{ comment_count }})</span>
|
||||
</h2>
|
||||
<p class="text-gray-400">分享您对这幅作品的看法</p>
|
||||
</div>
|
||||
|
||||
<!-- 评论表单(登录用户可见) -->
|
||||
{% if user.is_authenticated %}
|
||||
{% include "gallery/comments/comment_form.html" %}
|
||||
{% else %}
|
||||
<div class="bg-gray-900 border border-gray-800 rounded-lg p-6 mb-8">
|
||||
<p class="text-white mb-4">请登录后发表评论</p>
|
||||
<a href="{% url 'login' %}"
|
||||
class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition duration-200">
|
||||
立即登录
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 评论列表 -->
|
||||
{% include "gallery/comments/comment_list.html" %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 分享功能
|
||||
function shareArtwork() {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: '{{ artwork.title|escapejs }} - YITAO-REN GALLERY',
|
||||
text: '{{ artwork.description|truncatechars:100|escapejs }}',
|
||||
url: window.location.href,
|
||||
})
|
||||
.then(() => console.log('分享成功'))
|
||||
.catch((error) => console.log('分享失败:', error));
|
||||
} else {
|
||||
// 复制链接到剪贴板
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
.then(() => alert('链接已复制到剪贴板!'))
|
||||
.catch(err => alert('复制失败,请手动复制链接。'));
|
||||
}
|
||||
}
|
||||
|
||||
// 图片缩放功能
|
||||
const mainImage = document.querySelector('img[alt="{{ artwork.title }}"]');
|
||||
if (mainImage) {
|
||||
mainImage.addEventListener('click', function() {
|
||||
this.classList.toggle('cursor-zoom-out');
|
||||
this.classList.toggle('cursor-zoom-in');
|
||||
this.classList.toggle('scale-150');
|
||||
this.classList.toggle('origin-center');
|
||||
});
|
||||
}
|
||||
|
||||
// 键盘导航
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowLeft' && {% if prev_artwork %}true{% else %}false{% endif %}) {
|
||||
window.location.href = "{% if prev_artwork %}{% url 'artwork_detail' prev_artwork.slug %}{% endif %}";
|
||||
} else if (e.key === 'ArrowRight' && {% if next_artwork %}true{% else %}false{% endif %}) {
|
||||
window.location.href = "{% if next_artwork %}{% url 'artwork_detail' next_artwork.slug %}{% endif %}";
|
||||
} else if (e.key === 'Escape') {
|
||||
window.location.href = "{% url 'index' %}";
|
||||
}
|
||||
});
|
||||
|
||||
// 评论图片预览
|
||||
const commentImageInput = document.querySelector('input[name="image"]');
|
||||
if (commentImageInput) {
|
||||
const imagePreview = document.createElement('div');
|
||||
imagePreview.className = 'mt-4 hidden';
|
||||
imagePreview.innerHTML = `
|
||||
<p class="text-gray-400 text-sm mb-2">图片预览</p>
|
||||
<img class="max-w-full h-auto rounded-lg border border-gray-800 max-h-48 object-contain">
|
||||
<button type="button" class="mt-2 text-red-400 hover:text-red-300 text-sm">
|
||||
移除图片
|
||||
</button>
|
||||
`;
|
||||
commentImageInput.parentNode.insertBefore(imagePreview, commentImageInput.nextSibling);
|
||||
|
||||
const previewImg = imagePreview.querySelector('img');
|
||||
const removeBtn = imagePreview.querySelector('button');
|
||||
|
||||
commentImageInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
previewImg.src = e.target.result;
|
||||
imagePreview.classList.remove('hidden');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
imagePreview.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
removeBtn.addEventListener('click', function() {
|
||||
commentImageInput.value = '';
|
||||
imagePreview.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// 评论表单验证
|
||||
const commentForm = document.querySelector('form[action*="comment/add"]');
|
||||
if (commentForm) {
|
||||
commentForm.addEventListener('submit', function(e) {
|
||||
const textarea = this.querySelector('textarea[name="text"]');
|
||||
const fileInput = this.querySelector('input[name="image"]');
|
||||
|
||||
if (!textarea.value.trim() && !fileInput.files.length) {
|
||||
e.preventDefault();
|
||||
alert('请填写评论内容或上传图片');
|
||||
textarea.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 滚动到评论区域(如果URL中有#comments)
|
||||
if (window.location.hash === '#comments') {
|
||||
const commentsSection = document.getElementById('comments');
|
||||
if (commentsSection) {
|
||||
setTimeout(() => {
|
||||
commentsSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 评论删除确认
|
||||
document.addEventListener('submit', function(e) {
|
||||
if (e.target.action && e.target.action.includes('comment/delete')) {
|
||||
if (!confirm('确定要删除这条评论吗?此操作不可撤销。')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
83
gallery/templates/gallery/index.html
Executable file
83
gallery/templates/gallery/index.html
Executable file
@@ -0,0 +1,83 @@
|
||||
{% extends 'gallery/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-serif font-bold mb-4 animate-fade-in">
|
||||
YITAO-REN GALLERY
|
||||
</h1>
|
||||
<p class="text-gray-400 max-w-2xl mx-auto">
|
||||
探索现代摄影艺术的视觉盛宴,每一幅作品都是时光的印记。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-4 space-y-4 animate-slide-up mx-auto">
|
||||
|
||||
{% for artwork in artworks %}
|
||||
<div class="group relative overflow-hidden rounded-lg bg-dark-card break-inside-avoid mb-4 shadow-lg hover:shadow-xl transition-all duration-300">
|
||||
<a href="{% url 'artwork_detail' artwork.slug %}" class="block w-full">
|
||||
<div class="relative w-full">
|
||||
<img
|
||||
data-src="{{ artwork.thumbnail.url|default:artwork.image.url }}"
|
||||
alt="{{ artwork.title }}"
|
||||
class="lazy-image w-full h-auto object-cover block"
|
||||
loading="lazy"
|
||||
style="width: 100%; display: block;"
|
||||
>
|
||||
|
||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<div class="transform translate-y-4 group-hover:translate-y-0 transition-all duration-300 text-center p-4">
|
||||
<h3 class="text-white text-lg font-semibold mb-2 drop-shadow-md">{{ artwork.title }}</h3>
|
||||
{% if artwork.category %}
|
||||
<span class="inline-block px-3 py-1 bg-blue-600/90 text-white text-xs rounded-full shadow-sm">
|
||||
{{ artwork.category.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 md:hidden bg-dark-card">
|
||||
<h3 class="text-white font-semibold mb-1">{{ artwork.title }}</h3>
|
||||
{% if artwork.category %}
|
||||
<span class="text-blue-400 text-sm">{{ artwork.category.name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="break-inside-avoid w-full text-center py-16 bg-dark-card rounded-lg">
|
||||
<div class="text-gray-400 mb-4">
|
||||
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2">暂无作品</h3>
|
||||
<p class="text-gray-400">画廊正在准备中...</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div> <div class="mt-16 pt-8 border-t border-gray-800">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-400 mb-2">{{ artworks.count }}</div>
|
||||
<div class="text-gray-400">作品总数</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-400 mb-2">{{ categories|length }}</div>
|
||||
<div class="text-gray-400">分类数量</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
{% with total_views=artworks.aggregate.total_views %}
|
||||
<div class="text-3xl font-bold text-blue-400 mb-2">{{ total_views|default:"0" }}</div>
|
||||
{% endwith %}
|
||||
<div class="text-gray-400">总浏览次数</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-400 mb-2">2025</div>
|
||||
<div class="text-gray-400">成立年份</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
191
gallery/templates/gallery/search.html
Executable file
191
gallery/templates/gallery/search.html
Executable file
@@ -0,0 +1,191 @@
|
||||
{% extends 'gallery/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 面包屑导航 -->
|
||||
<nav class="mb-8">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-400">
|
||||
<li>
|
||||
<a href="{% url 'index' %}" class="hover:text-blue-400 transition-colors">Gallery</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="mx-2">/</span>
|
||||
</li>
|
||||
<li class="text-gray-300">搜索</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="max-w-2xl mx-auto mb-12">
|
||||
<form method="get" action="{% url 'search' %}" class="relative">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ query }}"
|
||||
placeholder="搜索作品标题、描述或分类..."
|
||||
class="w-full px-6 py-4 pl-14 bg-dark-card border border-gray-800 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="absolute left-5 top-1/2 transform -translate-y-1/2">
|
||||
<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if query %}
|
||||
<div class="mt-4 text-center">
|
||||
<p class="text-gray-400">
|
||||
搜索 "<span class="text-blue-400 font-medium">{{ query }}</span>" 的结果
|
||||
<span class="text-gray-300">({{ artworks.count }} 个作品)</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
{% if query %}
|
||||
{% if artworks %}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{% for artwork in artworks %}
|
||||
<a href="{% url 'artwork_detail' artwork.slug %}" class="group">
|
||||
<div class="relative overflow-hidden rounded-xl bg-dark-card transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl">
|
||||
<!-- 图片 -->
|
||||
<div class="aspect-square overflow-hidden">
|
||||
<img
|
||||
data-src="{{ artwork.thumbnail.url|default:artwork.image.url }}"
|
||||
alt="{{ artwork.title }}"
|
||||
class="lazy-image w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
|
||||
<!-- 作品信息 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="bg-black/80 backdrop-blur-sm rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-2 truncate">{{ artwork.title }}</h3>
|
||||
|
||||
{% if artwork.description %}
|
||||
<p class="text-gray-300 text-sm line-clamp-2 mb-3">
|
||||
{{ artwork.description|truncatechars:80 }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if artwork.category %}
|
||||
<div class="mb-3">
|
||||
<a href="{% url 'category' artwork.category.slug %}" class="inline-flex items-center px-3 py-1 bg-blue-600/20 text-blue-400 rounded-full text-xs hover:bg-blue-600/30 transition-colors">
|
||||
{{ artwork.category.name }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||
<span>{{ artwork.created_at|date:"Y年m月d日" }}</span>
|
||||
{% if artwork.view_count %}
|
||||
<span>{{ artwork.view_count }} 次浏览</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高亮匹配 -->
|
||||
{% if query in artwork.title or query in artwork.description %}
|
||||
<div class="absolute top-4 left-4">
|
||||
<span class="px-3 py-1 bg-yellow-600/90 text-white text-xs font-medium rounded-full backdrop-blur-sm">
|
||||
匹配
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 无结果 -->
|
||||
<div class="text-center py-20">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gray-800/50 mb-6">
|
||||
<svg class="w-12 h-12 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-300 mb-2">未找到相关作品</h3>
|
||||
<p class="text-gray-500 max-w-md mx-auto mb-8">
|
||||
没有找到与 "<span class="text-blue-400">{{ query }}</span>" 相关的作品。
|
||||
请尝试其他关键词或浏览所有作品。
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="{% url 'index' %}" class="inline-flex items-center justify-center px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
浏览所有作品
|
||||
</a>
|
||||
<button onclick="history.back()" class="inline-flex items-center justify-center px-6 py-3 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg transition-colors">
|
||||
返回上一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<!-- 搜索提示 -->
|
||||
<div class="text-center py-20">
|
||||
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gray-800/50 mb-6">
|
||||
<svg class="w-12 h-12 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-300 mb-2">搜索作品</h3>
|
||||
<p class="text-gray-500 max-w-md mx-auto mb-8">
|
||||
输入关键词搜索作品标题、描述或分类。
|
||||
支持中文和英文搜索。
|
||||
</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-3xl mx-auto">
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-gray-800">
|
||||
<div class="w-12 h-12 rounded-lg bg-blue-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-gray-300 mb-2">标题搜索</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
按作品标题关键词搜索,支持模糊匹配。
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-gray-800">
|
||||
<div class="w-12 h-12 rounded-lg bg-green-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-gray-300 mb-2">描述搜索</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
搜索作品描述中的关键词,了解作品背后的故事。
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-dark-card rounded-xl p-6 border border-gray-800">
|
||||
<div class="w-12 h-12 rounded-lg bg-purple-600/20 flex items-center justify-center mb-4">
|
||||
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="text-lg font-medium text-gray-300 mb-2">分类搜索</h4>
|
||||
<p class="text-gray-500 text-sm">
|
||||
按分类名称搜索,快速找到特定类型的作品。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
gallery/urls.py
Normal file
27
gallery/urls.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# 首页
|
||||
path('', views.index, name='index'),
|
||||
|
||||
# 作品详情页
|
||||
path('gallery/<slug:slug>/', views.ArtworkDetailView.as_view(), name='artwork_detail'),
|
||||
|
||||
# 关于页面
|
||||
path('about/', views.about, name='about'),
|
||||
|
||||
# 搜索页面
|
||||
path('search/', views.search, name='search'),
|
||||
|
||||
# 分类页面
|
||||
path('category/<slug:slug>/', views.category_view, name='category'),
|
||||
|
||||
# 用户认证
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
|
||||
# 评论功能
|
||||
path('artwork/<slug:artwork_slug>/comment/add/', views.add_comment, name='add_comment'),
|
||||
path('comment/<int:pk>/delete/', views.delete_comment, name='delete_comment'),
|
||||
]
|
||||
177
gallery/views.py
Normal file
177
gallery/views.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.generic import ListView, DetailView
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.core.paginator import Paginator
|
||||
from .models import Artwork, About, Comment
|
||||
from .forms import CommentForm
|
||||
|
||||
|
||||
def index(request):
|
||||
"""首页视图 - 展示所有作品"""
|
||||
artworks = Artwork.objects.all().order_by('order', '-created_at')
|
||||
context = {
|
||||
'artworks': artworks,
|
||||
'page_title': 'YITAO-REN GALLERY',
|
||||
}
|
||||
return render(request, 'gallery/index.html', context)
|
||||
|
||||
|
||||
class ArtworkDetailView(DetailView):
|
||||
"""作品详情页视图"""
|
||||
model = Artwork
|
||||
template_name = 'gallery/detail.html'
|
||||
context_object_name = 'artwork'
|
||||
slug_field = 'slug'
|
||||
slug_url_kwarg = 'slug'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
artwork = self.object
|
||||
|
||||
# 增加浏览次数
|
||||
artwork.increment_view_count()
|
||||
|
||||
# 获取评论(仅激活状态)
|
||||
comments = Comment.objects.filter(
|
||||
artwork=artwork,
|
||||
is_active=True
|
||||
).select_related('user').order_by('-created_at')
|
||||
|
||||
# 分页处理
|
||||
paginator = Paginator(comments, 10) # 每页10条
|
||||
page_number = self.request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context['comments'] = page_obj
|
||||
context['comment_form'] = CommentForm() if self.request.user.is_authenticated else None
|
||||
context['comment_count'] = comments.count()
|
||||
|
||||
# 获取相邻作品
|
||||
artworks = Artwork.objects.all().order_by('order', '-created_at')
|
||||
current_index = list(artworks).index(artwork)
|
||||
|
||||
# 前一个作品
|
||||
if current_index > 0:
|
||||
context['prev_artwork'] = artworks[current_index - 1]
|
||||
else:
|
||||
context['prev_artwork'] = None
|
||||
|
||||
# 后一个作品
|
||||
if current_index < len(artworks) - 1:
|
||||
context['next_artwork'] = artworks[current_index + 1]
|
||||
else:
|
||||
context['next_artwork'] = None
|
||||
|
||||
# 获取相关作品(同分类)
|
||||
if artwork.category:
|
||||
related_artworks = artwork.category.artwork_set.exclude(
|
||||
id=artwork.id
|
||||
).order_by('order', '-created_at')[:4]
|
||||
context['related_artworks'] = related_artworks
|
||||
else:
|
||||
context['related_artworks'] = Artwork.objects.none()
|
||||
|
||||
context['page_title'] = f'{artwork.title} - YITAO-REN GALLERY'
|
||||
return context
|
||||
|
||||
|
||||
def about(request):
|
||||
"""关于页面视图"""
|
||||
about_page = About.objects.first()
|
||||
context = {
|
||||
'about': about_page,
|
||||
'page_title': '关于 - YITAO-REN GALLERY',
|
||||
}
|
||||
return render(request, 'gallery/about.html', context)
|
||||
|
||||
|
||||
def search(request):
|
||||
"""搜索视图"""
|
||||
query = request.GET.get('q', '')
|
||||
artworks = Artwork.objects.all()
|
||||
|
||||
if query:
|
||||
artworks = artworks.filter(
|
||||
Q(title__icontains=query) |
|
||||
Q(description__icontains=query) |
|
||||
Q(category__name__icontains=query)
|
||||
)
|
||||
|
||||
context = {
|
||||
'artworks': artworks,
|
||||
'query': query,
|
||||
'page_title': f'搜索: {query} - YITAO-REN GALLERY',
|
||||
}
|
||||
return render(request, 'gallery/search.html', context)
|
||||
|
||||
|
||||
def category_view(request, slug):
|
||||
"""分类视图"""
|
||||
from .models import Category
|
||||
category = get_object_or_404(Category, slug=slug)
|
||||
artworks = Artwork.objects.filter(category=category).order_by('order', '-created_at')
|
||||
|
||||
context = {
|
||||
'category': category,
|
||||
'artworks': artworks,
|
||||
'page_title': f'{category.name} - YITAO-REN GALLERY',
|
||||
}
|
||||
return render(request, 'gallery/category.html', context)
|
||||
|
||||
|
||||
def login_view(request):
|
||||
"""处理用户登录"""
|
||||
if request.method == 'POST':
|
||||
username = request.POST.get('username')
|
||||
password = request.POST.get('password')
|
||||
user = authenticate(request, username=username, password=password)
|
||||
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
messages.success(request, '登录成功!')
|
||||
return redirect('index')
|
||||
else:
|
||||
messages.error(request, '用户名或密码错误')
|
||||
|
||||
return render(request, 'gallery/auth/login.html')
|
||||
|
||||
|
||||
@login_required
|
||||
def logout_view(request):
|
||||
"""处理用户登出"""
|
||||
logout(request)
|
||||
messages.success(request, '已成功登出')
|
||||
return redirect('index')
|
||||
|
||||
|
||||
@login_required
|
||||
def add_comment(request, artwork_slug):
|
||||
"""添加评论到指定作品"""
|
||||
artwork = get_object_or_404(Artwork, slug=artwork_slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = CommentForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
comment = form.save(commit=False)
|
||||
comment.artwork = artwork
|
||||
comment.user = request.user
|
||||
comment.save()
|
||||
messages.success(request, '评论已发布')
|
||||
else:
|
||||
for error in form.errors.values():
|
||||
messages.error(request, error)
|
||||
|
||||
return redirect('artwork_detail', slug=artwork_slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_comment(request, pk):
|
||||
"""删除评论(软删除)"""
|
||||
comment = get_object_or_404(Comment, pk=pk, user=request.user)
|
||||
comment.is_active = False
|
||||
comment.save()
|
||||
messages.success(request, '评论已删除')
|
||||
return redirect('artwork_detail', slug=comment.artwork.slug)
|
||||
Reference in New Issue
Block a user