Initial commit: Django gallery project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
279
gallery/models.py
Normal file
279
gallery/models.py
Normal file
@@ -0,0 +1,279 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.utils import timezone
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""作品分类模型"""
|
||||
name = models.CharField('分类名称', max_length=100)
|
||||
slug = models.SlugField('URL标识', max_length=100, unique=True, blank=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '作品分类'
|
||||
verbose_name_plural = '作品分类'
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 生成唯一的 slug
|
||||
if not self.slug:
|
||||
base_slug = slugify(self.name)
|
||||
if not base_slug: # 如果 slugify 返回空字符串
|
||||
base_slug = f'category-{self.id}' if self.id else 'category'
|
||||
|
||||
# 确保 slug 唯一
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while Category.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
||||
slug = f'{base_slug}-{counter}'
|
||||
counter += 1
|
||||
self.slug = slug
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Artwork(models.Model):
|
||||
"""摄影作品模型"""
|
||||
|
||||
title = models.CharField('作品标题', max_length=200)
|
||||
description = models.TextField('作品描述', blank=True)
|
||||
slug = models.SlugField('URL标识', max_length=200, unique=True, blank=True)
|
||||
|
||||
# 图片字段
|
||||
image = models.ImageField('作品图片', upload_to='artworks/%Y/%m/%d/')
|
||||
thumbnail = models.ImageField('缩略图', upload_to='thumbnails/%Y/%m/%d/', blank=True, null=True)
|
||||
|
||||
# 关联字段
|
||||
category = models.ForeignKey(
|
||||
Category,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name='作品分类',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# 元数据字段
|
||||
order = models.IntegerField('排序序号', default=0, help_text='数字越小越靠前')
|
||||
|
||||
# 时间字段
|
||||
created_at = models.DateTimeField('创建时间', default=timezone.now)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
# 统计字段
|
||||
view_count = models.PositiveIntegerField('浏览次数', default=0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '摄影作品'
|
||||
verbose_name_plural = '摄影作品'
|
||||
ordering = ['order', '-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 生成唯一的 slug
|
||||
if not self.slug:
|
||||
base_slug = slugify(self.title)
|
||||
if not base_slug: # 如果 slugify 返回空字符串
|
||||
base_slug = f'artwork-{self.id}' if self.id else 'artwork'
|
||||
|
||||
# 确保 slug 唯一
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while Artwork.objects.filter(slug=slug).exclude(pk=self.pk).exists():
|
||||
slug = f'{base_slug}-{counter}'
|
||||
counter += 1
|
||||
self.slug = slug
|
||||
|
||||
# 如果是新对象或图片被更新,生成缩略图
|
||||
if self.pk is None or 'image' in kwargs.get('update_fields', []):
|
||||
super().save(*args, **kwargs)
|
||||
self.generate_thumbnail()
|
||||
else:
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def generate_thumbnail(self):
|
||||
"""生成缩略图"""
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from django.core.files.base import ContentFile
|
||||
import os
|
||||
|
||||
if not self.image:
|
||||
return
|
||||
|
||||
# 打开原图
|
||||
img = Image.open(self.image)
|
||||
|
||||
# 计算目标像素总量:约90万像素(如1280x720)
|
||||
target_pixels = 900000
|
||||
|
||||
# 获取原图尺寸
|
||||
width, height = img.size
|
||||
current_pixels = width * height
|
||||
|
||||
# 计算缩放比例,使像素总量接近90万(但限制最大边不超过1600px)
|
||||
max_dimension = 1600
|
||||
|
||||
if current_pixels <= 0:
|
||||
# 如果图片尺寸无效,使用原尺寸但限制最大边
|
||||
if width > max_dimension or height > max_dimension:
|
||||
scale = max_dimension / max(width, height)
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
# 首先计算缩放至目标像素的比例
|
||||
scale_to_target = (target_pixels / current_pixels) ** 0.5
|
||||
|
||||
# 然后检查缩放后是否超过最大边限制
|
||||
new_width_temp = int(width * scale_to_target)
|
||||
new_height_temp = int(height * scale_to_target)
|
||||
|
||||
# 如果缩放后的尺寸超过最大边限制,使用最大边限制的比例
|
||||
if new_width_temp > max_dimension or new_height_temp > max_dimension:
|
||||
scale_to_max = max_dimension / max(new_width_temp, new_height_temp)
|
||||
scale = scale_to_target * scale_to_max
|
||||
else:
|
||||
scale = scale_to_target
|
||||
|
||||
new_width = int(width * scale)
|
||||
new_height = int(height * scale)
|
||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||
|
||||
# 保存到内存
|
||||
thumb_io = BytesIO()
|
||||
|
||||
# 保持原格式,如果是JPEG则优化
|
||||
# 如果img.format为空,根据文件扩展名判断格式
|
||||
format_to_use = img.format
|
||||
if not format_to_use:
|
||||
# 根据文件扩展名判断格式
|
||||
filename = os.path.basename(self.image.name)
|
||||
name, ext = os.path.splitext(filename)
|
||||
ext = ext.lower()
|
||||
if ext in ['.jpg', '.jpeg', '.jpe', '.jfif']:
|
||||
format_to_use = 'JPEG'
|
||||
elif ext in ['.png']:
|
||||
format_to_use = 'PNG'
|
||||
elif ext in ['.gif']:
|
||||
format_to_use = 'GIF'
|
||||
elif ext in ['.webp']:
|
||||
format_to_use = 'WEBP'
|
||||
else:
|
||||
# 默认使用JPEG格式
|
||||
format_to_use = 'JPEG'
|
||||
|
||||
if format_to_use == 'JPEG':
|
||||
img.save(thumb_io, format='JPEG', quality=85, optimize=True)
|
||||
else:
|
||||
img.save(thumb_io, format=format_to_use)
|
||||
|
||||
# 生成缩略图文件名
|
||||
filename = os.path.basename(self.image.name)
|
||||
name, ext = os.path.splitext(filename)
|
||||
thumb_filename = f'{name}_thumb{ext}'
|
||||
|
||||
# 保存缩略图
|
||||
self.thumbnail.save(
|
||||
thumb_filename,
|
||||
ContentFile(thumb_io.getvalue()),
|
||||
save=False
|
||||
)
|
||||
|
||||
super().save(update_fields=['thumbnail'])
|
||||
|
||||
|
||||
def increment_view_count(self):
|
||||
"""增加浏览次数"""
|
||||
self.view_count += 1
|
||||
self.save(update_fields=['view_count'])
|
||||
|
||||
def get_dynamic_grid_class(self):
|
||||
"""根据图片宽高比动态返回网格CSS类"""
|
||||
try:
|
||||
if not self.thumbnail:
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
# 尝试从缩略图获取尺寸
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
thumb_path = self.thumbnail.path
|
||||
if not os.path.exists(thumb_path):
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
with Image.open(thumb_path) as img:
|
||||
width, height = img.size
|
||||
aspect_ratio = width / height
|
||||
|
||||
# 根据宽高比返回不同的网格类
|
||||
if aspect_ratio > 1.5: # 很宽的图片
|
||||
return 'col-span-2 row-span-1' # 占2列宽
|
||||
elif aspect_ratio < 0.67: # 很高的图片
|
||||
return 'col-span-1 row-span-2' # 占2行高
|
||||
elif aspect_ratio > 1.2: # 中等宽度
|
||||
return 'col-span-2 row-span-1' # 占2列宽
|
||||
elif aspect_ratio < 0.83: # 中等高度
|
||||
return 'col-span-1 row-span-2' # 占2行高
|
||||
else: # 接近正方形
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
except Exception as e:
|
||||
# 发生错误时返回默认
|
||||
print(f"Error getting grid class for artwork {self.id}: {e}")
|
||||
return 'col-span-1 row-span-1'
|
||||
|
||||
|
||||
class About(models.Model):
|
||||
"""关于页面模型"""
|
||||
title = models.CharField('标题', max_length=200)
|
||||
content = models.TextField('内容')
|
||||
image = models.ImageField('图片', upload_to='about/', blank=True, null=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '关于页面'
|
||||
verbose_name_plural = '关于页面'
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 确保只有一个关于页面
|
||||
if not self.pk and About.objects.exists():
|
||||
# 如果已经存在关于页面,更新它而不是创建新的
|
||||
existing = About.objects.first()
|
||||
existing.title = self.title
|
||||
existing.content = self.content
|
||||
if self.image:
|
||||
existing.image = self.image
|
||||
existing.save()
|
||||
return existing
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
"""评论模型"""
|
||||
artwork = models.ForeignKey(Artwork, on_delete=models.CASCADE, verbose_name='作品')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='用户')
|
||||
text = models.TextField('评论内容', blank=True)
|
||||
image = models.ImageField('评论图片', upload_to='comments/%Y/%m/%d/', blank=True, null=True)
|
||||
created_at = models.DateTimeField('创建时间', auto_now_add=True)
|
||||
updated_at = models.DateTimeField('更新时间', auto_now=True)
|
||||
is_active = models.BooleanField('是否有效', default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '评论'
|
||||
verbose_name_plural = '评论'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} 对 {self.artwork.title} 的评论"
|
||||
Reference in New Issue
Block a user