279 lines
14 KiB
Python
279 lines
14 KiB
Python
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} 的评论" |