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} 的评论"