commit 8b57a7b66abcd970b0a8b121a9615629f43cb125 Author: Cafw Date: Wed Feb 25 16:47:17 2026 +0800 Initial commit: Django gallery project Co-Authored-By: Claude Sonnet 4.6 diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..33d6990 --- /dev/null +++ b/.env.example @@ -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" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4cc447 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100755 index 0000000..905ba1e --- /dev/null +++ b/README.md @@ -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 +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//) +- 左侧: 大图展示 + 返回按钮 +- 右侧: 标题 + 描述 (深蓝色背景卡片) + +### 关于页 (/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) +- 初始版本发布 +- 完整的功能实现 +- 响应式设计 +- 深色主题 +- 管理后台 \ No newline at end of file diff --git a/example_img/20260131_055243000_iOS.JPG b/example_img/20260131_055243000_iOS.JPG new file mode 100755 index 0000000..13587ab Binary files /dev/null and b/example_img/20260131_055243000_iOS.JPG differ diff --git a/example_img/20260131_055247000_iOS.JPG b/example_img/20260131_055247000_iOS.JPG new file mode 100755 index 0000000..db7b214 Binary files /dev/null and b/example_img/20260131_055247000_iOS.JPG differ diff --git a/example_img/20260131_055248000_iOS.JPG b/example_img/20260131_055248000_iOS.JPG new file mode 100755 index 0000000..0cf5969 Binary files /dev/null and b/example_img/20260131_055248000_iOS.JPG differ diff --git a/example_img/20260131_055251000_iOS.JPG b/example_img/20260131_055251000_iOS.JPG new file mode 100755 index 0000000..60e1d64 Binary files /dev/null and b/example_img/20260131_055251000_iOS.JPG differ diff --git a/example_img/20260131_055252000_iOS.JPG b/example_img/20260131_055252000_iOS.JPG new file mode 100755 index 0000000..3f51e0f Binary files /dev/null and b/example_img/20260131_055252000_iOS.JPG differ diff --git a/example_img/20260131_055254000_iOS.JPG b/example_img/20260131_055254000_iOS.JPG new file mode 100755 index 0000000..fdf694e Binary files /dev/null and b/example_img/20260131_055254000_iOS.JPG differ diff --git a/example_img/20260131_055255000_iOS.JPG b/example_img/20260131_055255000_iOS.JPG new file mode 100755 index 0000000..6778841 Binary files /dev/null and b/example_img/20260131_055255000_iOS.JPG differ diff --git a/example_img/20260131_055257000_iOS.JPG b/example_img/20260131_055257000_iOS.JPG new file mode 100755 index 0000000..cc6026b Binary files /dev/null and b/example_img/20260131_055257000_iOS.JPG differ diff --git a/example_img/20260131_055258000_iOS.JPG b/example_img/20260131_055258000_iOS.JPG new file mode 100755 index 0000000..3f22908 Binary files /dev/null and b/example_img/20260131_055258000_iOS.JPG differ diff --git a/example_img/20260131_055300000_iOS.JPG b/example_img/20260131_055300000_iOS.JPG new file mode 100755 index 0000000..9f7d166 Binary files /dev/null and b/example_img/20260131_055300000_iOS.JPG differ diff --git a/example_img/20260131_055302000_iOS.JPG b/example_img/20260131_055302000_iOS.JPG new file mode 100755 index 0000000..e534ba8 Binary files /dev/null and b/example_img/20260131_055302000_iOS.JPG differ diff --git a/example_img/20260131_055304000_iOS.JPG b/example_img/20260131_055304000_iOS.JPG new file mode 100755 index 0000000..c0414e7 Binary files /dev/null and b/example_img/20260131_055304000_iOS.JPG differ diff --git a/example_img/IMG_2338.JPG b/example_img/IMG_2338.JPG new file mode 100755 index 0000000..ec1cb6c Binary files /dev/null and b/example_img/IMG_2338.JPG differ diff --git a/gallery/__init__.py b/gallery/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gallery/admin.py b/gallery/admin.py new file mode 100644 index 0000000..37d40e5 --- /dev/null +++ b/gallery/admin.py @@ -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( + '', + obj.thumbnail.url + ) + return "-" + thumbnail_preview.short_description = '缩略图' + + def image_preview(self, obj): + if obj.image: + return format_html( + '', + 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( + '', + 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 = '欢迎使用画廊管理后台' \ No newline at end of file diff --git a/gallery/apps.py b/gallery/apps.py new file mode 100644 index 0000000..3ea6d8d --- /dev/null +++ b/gallery/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class GalleryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'gallery' + verbose_name = '画廊管理' \ No newline at end of file diff --git a/gallery/context_processors.py b/gallery/context_processors.py new file mode 100644 index 0000000..99878bd --- /dev/null +++ b/gallery/context_processors.py @@ -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, + } \ No newline at end of file diff --git a/gallery/forms.py b/gallery/forms.py new file mode 100644 index 0000000..6956c5a --- /dev/null +++ b/gallery/forms.py @@ -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 \ No newline at end of file diff --git a/gallery/management/__init__.py b/gallery/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gallery/management/commands/__init__.py b/gallery/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gallery/management/commands/import_example_images.py b/gallery/management/commands/import_example_images.py new file mode 100644 index 0000000..a09f696 --- /dev/null +++ b/gallery/management/commands/import_example_images.py @@ -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 \ No newline at end of file diff --git a/gallery/migrations/0001_initial.py b/gallery/migrations/0001_initial.py new file mode 100644 index 0000000..bdb64eb --- /dev/null +++ b/gallery/migrations/0001_initial.py @@ -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'], + }, + ), + ] \ No newline at end of file diff --git a/gallery/migrations/0002_remove_grid_size.py b/gallery/migrations/0002_remove_grid_size.py new file mode 100644 index 0000000..0a17953 --- /dev/null +++ b/gallery/migrations/0002_remove_grid_size.py @@ -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', + ), + ] diff --git a/gallery/migrations/0003_comment.py b/gallery/migrations/0003_comment.py new file mode 100644 index 0000000..c5fdf32 --- /dev/null +++ b/gallery/migrations/0003_comment.py @@ -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"], + }, + ), + ] diff --git a/gallery/migrations/__init__.py b/gallery/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gallery/models.py b/gallery/models.py new file mode 100644 index 0000000..7720956 --- /dev/null +++ b/gallery/models.py @@ -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} 的评论" \ No newline at end of file diff --git a/gallery/static/gallery/css/custom.css b/gallery/static/gallery/css/custom.css new file mode 100755 index 0000000..614d57b --- /dev/null +++ b/gallery/static/gallery/css/custom.css @@ -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; + } +} \ No newline at end of file diff --git a/gallery/static/gallery/js/lazy-load.js b/gallery/static/gallery/js/lazy-load.js new file mode 100755 index 0000000..a239727 --- /dev/null +++ b/gallery/static/gallery/js/lazy-load.js @@ -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. 基本用法: + * + * + * 2. 响应式图片: + * + * + * 3. 背景图片懒加载: + *
+ * + * 4. 手动触发加载: + * window.lazyLoadImages(); + */ \ No newline at end of file diff --git a/gallery/templates/gallery.zip b/gallery/templates/gallery.zip new file mode 100644 index 0000000..6799420 Binary files /dev/null and b/gallery/templates/gallery.zip differ diff --git a/gallery/templates/gallery/about.html b/gallery/templates/gallery/about.html new file mode 100755 index 0000000..19d4b56 --- /dev/null +++ b/gallery/templates/gallery/about.html @@ -0,0 +1,217 @@ +{% extends 'gallery/base.html' %} + +{% block content %} +
+ +
+

关于画廊

+

+ Yitao-Ren Gallery 致力于展示现代摄影艺术的独特魅力,
+ 每一幅作品都是摄影师对世界的独特诠释。 +

+
+ + +
+ {% if about %} + +
+ + {% if about.image %} +
+ {{ about.title }} +
+
+ {% else %} +
+
+ + + +

画廊图片

+
+
+ {% endif %} + + +
+

{{ about.title }}

+
+
+ {{ about.content|linebreaks }} +
+
+
+
+ {% else %} + +
+
+

YITAO-REN GALLERY

+ +
+

+ 成立于2026年,Yitao-Ren Gallery 是一个专注于现代摄影艺术展示的在线平台。 + 我们致力于发现和推广具有独特视角和艺术价值的摄影作品,为艺术家和艺术爱好者 + 搭建一个交流与展示的空间。 +

+ +

+ 画廊的名字来源于创始人对于摄影艺术的热爱与追求。"Yitao"代表着艺术的独特道路, + "Ren"象征着人文关怀。我们相信,每一幅摄影作品都是摄影师与世界对话的方式, + 是瞬间与永恒的完美结合。 +

+ +

我们的使命

+
    +
  • 展示高质量的现代摄影艺术作品
  • +
  • 支持新兴摄影艺术家的发展
  • +
  • 促进摄影艺术的交流与传播
  • +
  • 为艺术爱好者提供优质的观赏体验
  • +
+ +

画廊特色

+
+
+
专业策展
+

每幅作品都经过精心挑选和策展

+
+
+
高清展示
+

提供高质量的作品图片展示

+
+
+
艺术家支持
+

为艺术家提供展示和推广平台

+
+
+
社区交流
+

促进艺术爱好者之间的交流

+
+
+
+
+
+ {% endif %} + + +
+

我们的团队

+
+
+
+ YR +
+

Yitao Ren

+

创始人 & 策展人

+

拥有10年摄影艺术策展经验

+
+
+
+ LT +
+

Li Tao

+

技术总监

+

负责画廊平台的技术开发与维护

+
+
+
+ AW +
+

Art Wang

+

艺术顾问

+

资深艺术评论家,摄影艺术专家

+
+
+
+ + +
+

联系我们

+
+
+
+

画廊地址

+

+ 北京市朝阳区艺术大道123号
+ 艺术创意园区A座3层 +

+
+
+

开放时间

+

+ 周二至周日:10:00 - 18:00
+ 周一闭馆 +

+
+
+
+
+

联系方式

+

+ 电话:+86 10 1234 5678
+ 邮箱:info@yitaoren-gallery.com
+ 微信:YitaoRenGallery +

+
+
+

关注我们

+ +
+
+
+
+
+
+ +{% block extra_js %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/gallery/templates/gallery/auth/login.html b/gallery/templates/gallery/auth/login.html new file mode 100644 index 0000000..0573b50 --- /dev/null +++ b/gallery/templates/gallery/auth/login.html @@ -0,0 +1,63 @@ +{% extends "gallery/base.html" %} + +{% block title %}登录 - {{ site_name }}{% endblock %} + +{% block content %} +
+
+
+

登录账户

+

请使用管理员提供的账户登录

+
+ +
+ {% csrf_token %} + + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+

+ 需要账户?请联系管理员创建 +

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/gallery/auth/logout.html b/gallery/templates/gallery/auth/logout.html new file mode 100644 index 0000000..0f88677 --- /dev/null +++ b/gallery/templates/gallery/auth/logout.html @@ -0,0 +1,26 @@ +{% extends "gallery/base.html" %} + +{% block title %}登出 - {{ site_name }}{% endblock %} + +{% block content %} +
+
+
+

确认登出

+

您确定要退出登录吗?

+ +
+ {% csrf_token %} + + + 取消 + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/gallery/base.html b/gallery/templates/gallery/base.html new file mode 100755 index 0000000..3090a6e --- /dev/null +++ b/gallery/templates/gallery/base.html @@ -0,0 +1,284 @@ +{% load static %} + + + + + + {% block title %}{{ page_title|default:site_name }}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% block extra_css %}{% endblock %} + + + + + + +
+ {% block content %}{% endblock %} +
+ + +
+
+
+
+ {{ copyright_text }} +
+
+ All artworks are copyrighted by their respective owners. +
+
+
+
+ + + + {% block extra_js %}{% endblock %} + + \ No newline at end of file diff --git a/gallery/templates/gallery/category.html b/gallery/templates/gallery/category.html new file mode 100755 index 0000000..d09779c --- /dev/null +++ b/gallery/templates/gallery/category.html @@ -0,0 +1,90 @@ +{% extends 'gallery/base.html' %} + +{% block content %} +
+ + + + +
+

{{ category.name }}

+

+ {{ artworks.count }} 个作品 +

+
+ + + {% if artworks %} + + {% else %} + +
+
+ + + +
+

暂无作品

+

+ 该分类下暂时没有作品,请稍后再来查看。 +

+ + + + + 返回 Gallery + +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/gallery/templates/gallery/comments/comment_form.html b/gallery/templates/gallery/comments/comment_form.html new file mode 100644 index 0000000..1e44056 --- /dev/null +++ b/gallery/templates/gallery/comments/comment_form.html @@ -0,0 +1,42 @@ +
+ {% csrf_token %} + +
+

发表评论

+ + +
+ {{ comment_form.text }} + {% if comment_form.text.errors %} +
{{ comment_form.text.errors }}
+ {% endif %} +
+ + +
+ + {{ comment_form.image }} +

支持 JPG、PNG、GIF、WEBP 格式,最大 5MB

+ {% if comment_form.image.errors %} +
{{ comment_form.image.errors }}
+ {% endif %} +
+ + + {% if comment_form.non_field_errors %} +
+ {% for error in comment_form.non_field_errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} + + +
+ +
+
+
\ No newline at end of file diff --git a/gallery/templates/gallery/comments/comment_item.html b/gallery/templates/gallery/comments/comment_item.html new file mode 100644 index 0000000..1dfe9f6 --- /dev/null +++ b/gallery/templates/gallery/comments/comment_item.html @@ -0,0 +1,35 @@ +
+
+
+
+ {{ comment.user.username|first|upper }} +
+
+

{{ comment.user.username }}

+

{{ comment.created_at|date:"Y年m月d日 H:i" }}

+
+
+ {% if comment.user == user %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ + {% if comment.text %} +
{{ comment.text|linebreaks }}
+ {% endif %} + + {% if comment.image %} +
+ 评论图片 +
+ {% endif %} +
\ No newline at end of file diff --git a/gallery/templates/gallery/comments/comment_list.html b/gallery/templates/gallery/comments/comment_list.html new file mode 100644 index 0000000..58320c7 --- /dev/null +++ b/gallery/templates/gallery/comments/comment_list.html @@ -0,0 +1,32 @@ +
+ {% for comment in comments %} + {% include "gallery/comments/comment_item.html" %} + {% empty %} +
+

暂无评论,成为第一个评论者吧!

+
+ {% endfor %} + + + {% if comments.paginator.num_pages > 1 %} +
+ {% if comments.has_previous %} + + 上一页 + + {% endif %} + + + 第 {{ comments.number }} / {{ comments.paginator.num_pages }} 页 + + + {% if comments.has_next %} + + 下一页 + + {% endif %} +
+ {% endif %} +
\ No newline at end of file diff --git a/gallery/templates/gallery/detail.html b/gallery/templates/gallery/detail.html new file mode 100755 index 0000000..8f2eb05 --- /dev/null +++ b/gallery/templates/gallery/detail.html @@ -0,0 +1,337 @@ +{% extends 'gallery/base.html' %} + +{% block content %} +
+ + + + + + + +
+ +
+ +
+ {{ artwork.title }} + + +
+
+ {{ artwork.created_at|date:"Y年m月d日" }} + {% if artwork.view_count %} + · {{ artwork.view_count }} 次浏览 + {% endif %} +
+
+
+ + + {% if prev_artwork or next_artwork %} +
+ {% if prev_artwork %} + + + + +
+
上一幅
+
{{ prev_artwork.title|truncatechars:20 }}
+
+
+ {% else %} +
+ {% endif %} + + {% if next_artwork %} + +
+
下一幅
+
{{ next_artwork.title|truncatechars:20 }}
+
+ + + +
+ {% else %} +
+ {% endif %} +
+ {% endif %} +
+ + +
+ +
+

{{ artwork.title }}

+ + {% if artwork.category %} +
+ + {{ artwork.category.name }} + + + {{ artwork.created_at|date:"Y年m月d日" }} +
+ {% endif %} +
+ + +
+
+

作品描述

+
+ {{ artwork.description|linebreaks }} +
+
+
+ + +
+

作品信息

+
+
+
作品编号
+
ART-{{ artwork.id|stringformat:"04d" }}
+
+
+
上传时间
+
{{ artwork.created_at|date:"Y-m-d H:i" }}
+
+
+
最后更新
+
{{ artwork.updated_at|date:"Y-m-d H:i" }}
+
+
+
浏览次数
+
{{ artwork.view_count }}
+
+
+
排序序号
+
{{ artwork.order }}
+
+
+
+ + +
+ + + + + + + 下载原图 + +
+
+
+ + + {% if related_artworks %} +
+

同分类作品

+
+ {% for related in related_artworks %} + +
+ {{ related.title }} +
+
+

+ {{ related.title }} +

+
+
+
+ {% endfor %} +
+
+ {% endif %} + + +
+
+

+ 评论 ({{ comment_count }}) +

+

分享您对这幅作品的看法

+
+ + + {% if user.is_authenticated %} + {% include "gallery/comments/comment_form.html" %} + {% else %} +
+

请登录后发表评论

+ + 立即登录 + +
+ {% endif %} + + + {% include "gallery/comments/comment_list.html" %} +
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/gallery/templates/gallery/index.html b/gallery/templates/gallery/index.html new file mode 100755 index 0000000..555e392 --- /dev/null +++ b/gallery/templates/gallery/index.html @@ -0,0 +1,83 @@ +{% extends 'gallery/base.html' %} + +{% block content %} +
+
+

+ YITAO-REN GALLERY +

+

+ 探索现代摄影艺术的视觉盛宴,每一幅作品都是时光的印记。 +

+
+ +
+
+
+
{{ artworks.count }}
+
作品总数
+
+
+
{{ categories|length }}
+
分类数量
+
+
+ {% with total_views=artworks.aggregate.total_views %} +
{{ total_views|default:"0" }}
+ {% endwith %} +
总浏览次数
+
+
+
2025
+
成立年份
+
+
+
+
+{% endblock %} diff --git a/gallery/templates/gallery/search.html b/gallery/templates/gallery/search.html new file mode 100755 index 0000000..871783c --- /dev/null +++ b/gallery/templates/gallery/search.html @@ -0,0 +1,191 @@ +{% extends 'gallery/base.html' %} + +{% block content %} +
+ + + + +
+
+
+ +
+ + + +
+ +
+
+ + {% if query %} +
+

+ 搜索 "{{ query }}" 的结果 + ({{ artworks.count }} 个作品) +

+
+ {% endif %} +
+ + + {% if query %} + {% if artworks %} +
+ {% for artwork in artworks %} + +
+ +
+ {{ artwork.title }} +
+ + +
+ + +
+
+

{{ artwork.title }}

+ + {% if artwork.description %} +

+ {{ artwork.description|truncatechars:80 }} +

+ {% endif %} + + {% if artwork.category %} +
+ {% endif %} + +
+ {{ artwork.created_at|date:"Y年m月d日" }} + {% if artwork.view_count %} + {{ artwork.view_count }} 次浏览 + {% endif %} +
+
+
+ + + {% if query in artwork.title or query in artwork.description %} +
+ + 匹配 + +
+ {% endif %} +
+ + {% endfor %} +
+ {% else %} + +
+
+ + + +
+

未找到相关作品

+

+ 没有找到与 "{{ query }}" 相关的作品。 + 请尝试其他关键词或浏览所有作品。 +

+
+ + + + + 浏览所有作品 + + +
+
+ {% endif %} + {% else %} + +
+
+ + + +
+

搜索作品

+

+ 输入关键词搜索作品标题、描述或分类。 + 支持中文和英文搜索。 +

+
+
+
+ + + +
+

标题搜索

+

+ 按作品标题关键词搜索,支持模糊匹配。 +

+
+
+
+ + + +
+

描述搜索

+

+ 搜索作品描述中的关键词,了解作品背后的故事。 +

+
+
+
+ + + +
+

分类搜索

+

+ 按分类名称搜索,快速找到特定类型的作品。 +

+
+
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/gallery/urls.py b/gallery/urls.py new file mode 100644 index 0000000..ebc1ea2 --- /dev/null +++ b/gallery/urls.py @@ -0,0 +1,27 @@ +from django.urls import path +from . import views + +urlpatterns = [ + # 首页 + path('', views.index, name='index'), + + # 作品详情页 + path('gallery//', views.ArtworkDetailView.as_view(), name='artwork_detail'), + + # 关于页面 + path('about/', views.about, name='about'), + + # 搜索页面 + path('search/', views.search, name='search'), + + # 分类页面 + path('category//', views.category_view, name='category'), + + # 用户认证 + path('login/', views.login_view, name='login'), + path('logout/', views.logout_view, name='logout'), + + # 评论功能 + path('artwork//comment/add/', views.add_comment, name='add_comment'), + path('comment//delete/', views.delete_comment, name='delete_comment'), +] \ No newline at end of file diff --git a/gallery/views.py b/gallery/views.py new file mode 100644 index 0000000..1ce0a8f --- /dev/null +++ b/gallery/views.py @@ -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) \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..c6c3a72 --- /dev/null +++ b/manage.py @@ -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() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..57c5b51 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..40161b4 --- /dev/null +++ b/setup.sh @@ -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 "" \ No newline at end of file diff --git a/yitao_gallery/__init__.py b/yitao_gallery/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yitao_gallery/asgi.py b/yitao_gallery/asgi.py new file mode 100644 index 0000000..36a6155 --- /dev/null +++ b/yitao_gallery/asgi.py @@ -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() \ No newline at end of file diff --git a/yitao_gallery/settings.py b/yitao_gallery/settings.py new file mode 100644 index 0000000..d1f5b78 --- /dev/null +++ b/yitao_gallery/settings.py @@ -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' diff --git a/yitao_gallery/urls.py b/yitao_gallery/urls.py new file mode 100644 index 0000000..4f5c2ee --- /dev/null +++ b/yitao_gallery/urls.py @@ -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) \ No newline at end of file diff --git a/yitao_gallery/wsgi.py b/yitao_gallery/wsgi.py new file mode 100644 index 0000000..2e88e3d --- /dev/null +++ b/yitao_gallery/wsgi.py @@ -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() \ No newline at end of file