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