diff --git a/backend/competition/admin.py b/backend/competition/admin.py index ed26af2..47d2504 100644 --- a/backend/competition/admin.py +++ b/backend/competition/admin.py @@ -19,6 +19,19 @@ class CompetitionAdmin(ModelAdmin): search_fields = ['title', 'description'] inlines = [ScoreDimensionInline] + fieldsets = ( + ('基本信息', { + 'fields': ('title', 'description', 'rule_description', 'condition_description') + }), + ('封面设置', { + 'fields': ('cover_image', 'cover_image_url'), + 'description': '封面图可以上传本地图片,也可以填写外部链接,优先显示本地上传的图片' + }), + ('时间和状态', { + 'fields': ('start_time', 'end_time', 'status', 'is_active') + }), + ) + actions = ['make_published', 'make_ended'] def make_published(self, request, queryset): @@ -51,6 +64,19 @@ class ProjectAdmin(ModelAdmin): search_fields = ['title', 'contestant__user__nickname'] inlines = [ProjectFileInline] readonly_fields = ['final_score'] + + fieldsets = ( + ('基本信息', { + 'fields': ('competition', 'contestant', 'title', 'description', 'team_info') + }), + ('封面设置', { + 'fields': ('cover_image', 'cover_image_url'), + 'description': '封面图可以上传本地图片,也可以填写外部链接,优先显示本地上传的图片' + }), + ('状态和得分', { + 'fields': ('status', 'final_score') + }), + ) @admin.register(Score) class ScoreAdmin(ModelAdmin): diff --git a/backend/competition/migrations/0002_competition_cover_image_url_project_cover_image_url.py b/backend/competition/migrations/0002_competition_cover_image_url_project_cover_image_url.py new file mode 100644 index 0000000..ae565c5 --- /dev/null +++ b/backend/competition/migrations/0002_competition_cover_image_url_project_cover_image_url.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-03-10 02:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='competition', + name='cover_image_url', + field=models.URLField(blank=True, help_text='优先使用上传的图片', null=True, verbose_name='封面图URL'), + ), + migrations.AddField( + model_name='project', + name='cover_image_url', + field=models.URLField(blank=True, help_text='优先使用上传的图片', null=True, verbose_name='项目封面URL'), + ), + ] diff --git a/backend/competition/models.py b/backend/competition/models.py index fa62f60..53289d8 100644 --- a/backend/competition/models.py +++ b/backend/competition/models.py @@ -20,6 +20,7 @@ class Competition(models.Model): condition_description = models.TextField(verbose_name="参赛条件说明", blank=True) cover_image = models.ImageField(upload_to='competitions/covers/', verbose_name="封面图", null=True, blank=True) + cover_image_url = models.URLField(verbose_name="封面图URL", null=True, blank=True, help_text="优先使用上传的图片") start_time = models.DateTimeField(verbose_name="开始时间") end_time = models.DateTimeField(verbose_name="结束时间") @@ -111,6 +112,7 @@ class Project(models.Model): team_info = models.TextField(verbose_name="团队介绍", blank=True) cover_image = models.ImageField(upload_to='competitions/projects/covers/', verbose_name="项目封面", null=True, blank=True) + cover_image_url = models.URLField(verbose_name="项目封面URL", null=True, blank=True, help_text="优先使用上传的图片") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="状态") diff --git a/backend/competition/serializers.py b/backend/competition/serializers.py index 6267974..b0411ec 100644 --- a/backend/competition/serializers.py +++ b/backend/competition/serializers.py @@ -9,13 +9,20 @@ class ScoreDimensionSerializer(serializers.ModelSerializer): class CompetitionSerializer(serializers.ModelSerializer): score_dimensions = ScoreDimensionSerializer(many=True, read_only=True) + display_cover_image = serializers.SerializerMethodField() class Meta: model = Competition fields = ['id', 'title', 'description', 'rule_description', 'condition_description', - 'cover_image', 'start_time', 'end_time', 'status', 'is_active', + 'cover_image', 'cover_image_url', 'display_cover_image', + 'start_time', 'end_time', 'status', 'is_active', 'score_dimensions', 'created_at'] + def get_display_cover_image(self, obj): + if obj.cover_image: + return obj.cover_image.url + return obj.cover_image_url + class CompetitionEnrollmentSerializer(serializers.ModelSerializer): user = WeChatUserSerializer(read_only=True) @@ -41,11 +48,13 @@ class ProjectFileSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer): files = ProjectFileSerializer(many=True, read_only=True) contestant_info = serializers.SerializerMethodField() + display_cover_image = serializers.SerializerMethodField() class Meta: model = Project fields = ['id', 'competition', 'contestant', 'title', 'description', 'team_info', - 'cover_image', 'status', 'final_score', 'files', 'contestant_info', 'created_at'] + 'cover_image', 'cover_image_url', 'display_cover_image', + 'status', 'final_score', 'files', 'contestant_info', 'created_at'] read_only_fields = ['final_score', 'contestant'] def get_contestant_info(self, obj): @@ -54,6 +63,11 @@ class ProjectSerializer(serializers.ModelSerializer): "avatar_url": obj.contestant.user.avatar_url } + def get_display_cover_image(self, obj): + if obj.cover_image: + return obj.cover_image.url + return obj.cover_image_url + class ScoreSerializer(serializers.ModelSerializer): judge_name = serializers.CharField(source='judge.user.nickname', read_only=True) dimension_name = serializers.CharField(source='dimension.name', read_only=True) diff --git a/backend/config/settings.py b/backend/config/settings.py index ba33455..af76f80 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -317,6 +317,37 @@ UNFOLD = { }, ], }, + { + "title": "比赛管理", + "separator": True, + "items": [ + { + "title": "比赛列表", + "icon": "emoji_events", + "link": reverse_lazy("admin:competition_competition_changelist"), + }, + { + "title": "比赛人员/报名", + "icon": "group_add", + "link": reverse_lazy("admin:competition_competitionenrollment_changelist"), + }, + { + "title": "参赛项目", + "icon": "lightbulb", + "link": reverse_lazy("admin:competition_project_changelist"), + }, + { + "title": "评分记录", + "icon": "score", + "link": reverse_lazy("admin:competition_score_changelist"), + }, + { + "title": "评委评语", + "icon": "rate_review", + "link": reverse_lazy("admin:competition_comment_changelist"), + }, + ], + }, { "title": "系统配置", "separator": True,