Initial commit: Django gallery project

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 16:47:17 +08:00
commit 8b57a7b66a
53 changed files with 3633 additions and 0 deletions

36
.env.example Executable file
View File

@@ -0,0 +1,36 @@
# Django Settings
DEBUG=True
SECRET_KEY=your-secret-key-here-change-in-production
# Database
DATABASE_URL=sqlite:///db.sqlite3
# Email Settings (optional)
EMAIL_HOST=localhost
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
# Security Settings (for production)
# ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
# CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
# File Upload Settings
FILE_UPLOAD_MAX_MEMORY_SIZE=5242880
DATA_UPLOAD_MAX_MEMORY_SIZE=5242880
# Image Settings
IMAGE_MAX_WIDTH=1920
IMAGE_MAX_HEIGHT=1080
THUMBNAIL_SIZE=400
# Site Settings
SITE_NAME="YITAO-REN GALLERY"
COPYRIGHT_TEXT="© 2026 Yitao-Ren Gallery & iTao TV"
# Timezone
TIME_ZONE="Asia/Shanghai"
# Language
LANGUAGE_CODE="zh-hans"

47
.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.pyc
.Python
*.egg
*.egg-info/
dist/
build/
.eggs/
.pytest_cache/
.mypy_cache/
# Django
*.sqlite3
db.sqlite3
local_settings.py
staticfiles/
media/
# Environment
.env
.env.*
!.env.example
# Logs
logs/
*.log
# Tailwind CSS binary
tailwindcss
# macOS
.DS_Store
.AppleDouble
.LSOverride
# IDE
.vscode/
.idea/
*.swp
*.swo
# Claude Code
.claude/

189
README.md Executable file
View File

@@ -0,0 +1,189 @@
# Yitao-Ren Gallery
一个深色主题的现代摄影作品展示网站使用Django和Tailwind CSS构建。
## 功能特点
- 🎨 深色主题设计,极简现代风格
- 📱 完全响应式设计,适配移动端
- 🖼️ 瀑布流网格布局展示作品
- 🔍 图片懒加载,提升性能
- 📝 作品详情页左右分栏布局
- 👤 关于页面展示画廊信息
- 🔧 Django Admin后台管理
## 技术栈
- **后端**: Django 4.2
- **前端**: Tailwind CSS 3.3
- **数据库**: SQLite (开发) / PostgreSQL (生产)
- **图片处理**: Pillow
- **字体**: Noto Sans SC (中文), Playfair Display (英文标题)
## 安装步骤
### 1. 克隆项目
```bash
git clone <repository-url>
cd yitao_gallery
```
### 2. 创建虚拟环境
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或
venv\Scripts\activate # Windows
```
### 3. 安装依赖
```bash
pip install -r requirements.txt
```
### 4. 配置环境变量
```bash
cp .env.example .env
# 编辑.env文件设置必要的配置
```
### 5. 运行数据库迁移
```bash
python manage.py migrate
```
### 6. 导入示例数据
```bash
python manage.py import_example_images
```
### 7. 创建超级用户
```bash
python manage.py createsuperuser
```
### 8. 运行开发服务器
```bash
python manage.py runserver
```
访问 http://localhost:8000 查看网站http://localhost:8000/admin 进入管理后台。
## 项目结构
```
yitao_gallery/
├── yitao_gallery/ # Django项目配置
├── gallery/ # 画廊应用
│ ├── models.py # 数据模型
│ ├── views.py # 视图函数
│ ├── templates/ # 模板文件
│ └── static/ # 静态文件
├── media/ # 上传的媒体文件
└── fixtures/ # 初始数据
```
## 数据模型
### Artwork (作品)
- 标题、描述、图片、缩略图
- slug (用于URL)
- 分类 (外键)
- 创建/更新时间
- 排序字段
- 网格尺寸字段
### Category (分类)
- 名称、slug
### About (关于)
- 标题、内容、图片
## 页面说明
### 首页 (/)
- 瀑布流网格展示所有作品
- 图片悬停效果
- 懒加载图片
- 点击图片进入详情页
### 作品详情页 (/gallery/<slug>/)
- 左侧: 大图展示 + 返回按钮
- 右侧: 标题 + 描述 (深蓝色背景卡片)
### 关于页 (/about/)
- 画廊介绍信息
- 联系方式等
## 设计规范
### 颜色方案
- 背景: #0a0a0a (深黑色)
- 文字: #f5f5f5 (浅灰色)
- 强调色: #3b82f6 (蓝色)
- 卡片背景: #1e293b (深蓝色)
### 字体
- 英文标题: Playfair Display
- 中文正文: Noto Sans SC
- 导航栏: 衬线字体,全大写
### 间距
- 图片间距: 12px
- 内容内边距: 24px
- 响应式断点: sm:640px, md:768px, lg:1024px, xl:1280px
## 开发说明
### 添加新作品
1. 登录管理后台 (/admin)
2. 进入 "Artworks" 部分
3. 点击 "Add Artwork"
4. 上传图片,填写信息
5. 保存即可
### 自定义样式
- 主要样式在 `templates/gallery/base.html` 中使用Tailwind CSS
- 自定义CSS在 `static/gallery/css/custom.css`
- JavaScript在 `static/gallery/js/lazy-load.js`
### 部署建议
#### 生产环境配置
1. 设置 `DEBUG = False`
2. 配置 `ALLOWED_HOSTS`
3. 使用PostgreSQL数据库
4. 配置静态文件和媒体文件服务
5. 设置CSRF和SESSION安全选项
#### 性能优化
- 启用缓存
- 使用CDN分发静态文件
- 配置图片压缩
- 启用Gzip压缩
## 贡献指南
1. Fork 项目
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建Pull Request
## 许可证
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
## 联系信息
- 项目维护者: Yitao-Ren Gallery
- 网站: © 2026 Yitao-Ren Gallery & iTao TV
## 更新日志
### v1.0.0 (2026-02-05)
- 初始版本发布
- 完整的功能实现
- 响应式设计
- 深色主题
- 管理后台

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
example_img/IMG_2338.JPG Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

0
gallery/__init__.py Normal file
View File

160
gallery/admin.py Normal file
View 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
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class GalleryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gallery'
verbose_name = '画廊管理'

View 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
View 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

View File

View File

View 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

View 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'],
},
),
]

View 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',
),
]

View 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"],
},
),
]

View File

279
gallery/models.py Normal file
View 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} 的评论"

View 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;
}
}

View 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();
*/

Binary file not shown.

View 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 %}

View 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 %}

View 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 %}

View 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>

View 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 %}

View 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>

View 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>

View 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>

View 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 %}

View 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 %}

View 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
View 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
View 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)

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yitao_gallery.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

6
requirements.txt Executable file
View File

@@ -0,0 +1,6 @@
Django==4.2.0
Pillow==10.0.0
django-cleanup==8.0.0
django-imagekit==4.1.0
django-taggit==4.0.0
python-slugify==8.0.1

71
setup.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# YITAO-REN GALLERY 项目安装脚本
set -e
echo "🚀 开始安装 YITAO-REN GALLERY..."
# 检查Python版本
echo "📋 检查Python版本..."
python --version || { echo "❌ Python未安装"; exit 1; }
# 创建虚拟环境
echo "🔧 创建虚拟环境..."
python -m venv venv
# 激活虚拟环境
echo "🔧 激活虚拟环境..."
source venv/bin/activate
# 升级pip
echo "📦 升级pip..."
pip install --upgrade pip
# 安装依赖
echo "📦 安装依赖..."
pip install -r requirements.txt
# 复制环境变量文件
echo "⚙️ 配置环境变量..."
if [ ! -f .env ]; then
cp .env.example .env
echo "✅ 已创建 .env 文件,请根据需要修改配置"
fi
# 运行数据库迁移
echo "🗄️ 运行数据库迁移..."
python manage.py migrate
# 导入示例数据
echo "🖼️ 导入示例图片..."
python manage.py import_example_images
# 收集静态文件
echo "📁 收集静态文件..."
python manage.py collectstatic --noinput
# 创建超级用户
echo "👤 创建超级用户..."
read -p "是否创建超级用户?(y/n): " create_superuser
if [[ $create_superuser == "y" || $create_superuser == "Y" ]]; then
python manage.py createsuperuser
fi
echo ""
echo "🎉 安装完成!"
echo ""
echo "📋 运行以下命令启动项目:"
echo " source venv/bin/activate"
echo " python manage.py runserver"
echo ""
echo "🌐 访问地址:"
echo " - 网站首页: http://localhost:8000"
echo " - 管理后台: http://localhost:8000/admin"
echo ""
echo "🔧 其他命令:"
echo " - 导入示例图片: python manage.py import_example_images"
echo " - 创建迁移文件: python manage.py makemigrations"
echo " - 应用迁移: python manage.py migrate"
echo " - 收集静态文件: python manage.py collectstatic"
echo ""

View File

16
yitao_gallery/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for yitao_gallery project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yitao_gallery.settings')
application = get_asgi_application()

160
yitao_gallery/settings.py Normal file
View File

@@ -0,0 +1,160 @@
"""
Django settings for yitao_gallery project.
Generated by 'django-admin startproject' using Django 4.2.0.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-!@#$%^&*()_+yitao-ren-gallery-secret-key-change-in-production'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["localhost", "106.15.60.252", "gallery.lizexua.com"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'django_cleanup.apps.CleanupConfig',
# Local apps
'gallery.apps.GalleryConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'yitao_gallery.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'gallery.context_processors.site_settings',
],
},
},
]
WSGI_APPLICATION = 'yitao_gallery.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Media files
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# File upload settings
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB
# Image settings
IMAGE_MAX_WIDTH = 1920
IMAGE_MAX_HEIGHT = 1080
THUMBNAIL_SIZE = (400, 400)
# Site settings
SITE_NAME = "YITAO-REN GALLERY"
COPYRIGHT_TEXT = "© 2026 Yitao-Ren Gallery & iTao TV"
# Security settings for production (commented out for development)
# SECURE_SSL_REDIRECT = True
# SESSION_COOKIE_SECURE = True
# CSRF_COOKIE_SECURE = True
# SECURE_BROWSER_XSS_FILTER = True
# SECURE_CONTENT_TYPE_NOSNIFF = True
# X_FRAME_OPTIONS = 'DENY'

29
yitao_gallery/urls.py Normal file
View File

@@ -0,0 +1,29 @@
"""
URL configuration for yitao_gallery project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('gallery.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
yitao_gallery/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for yitao_gallery project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yitao_gallery.settings')
application = get_wsgi_application()