diff --git a/backend/community/admin.py b/backend/community/admin.py index a1a33e3..3dc2d1f 100644 --- a/backend/community/admin.py +++ b/backend/community/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin from django.utils.html import format_html +from django.urls import path, reverse +from django.shortcuts import redirect from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement @@ -28,6 +30,83 @@ class TopicMediaInline(TabularInline): readonly_fields = ('created_at',) can_delete = True +class OrderableAdminMixin: + """ + 为 Admin 添加排序功能的 Mixin + 提供上移、下移按钮,直接交换 order 值 + """ + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path('/move-up/', self.admin_site.admin_view(self.move_up_view), name=f'{self.model._meta.app_label}_{self.model._meta.model_name}_move_up'), + path('/move-down/', self.admin_site.admin_view(self.move_down_view), name=f'{self.model._meta.app_label}_{self.model._meta.model_name}_move_down'), + ] + return custom_urls + urls + + def move_up_view(self, request, object_id): + obj = self.get_object(request, object_id) + if obj: + # 找到排在它前面的一个 (order 小于它的最大值) + prev_obj = self.model.objects.filter(order__lt=obj.order).order_by('-order').first() + if prev_obj: + # 交换 + obj.order, prev_obj.order = prev_obj.order, obj.order + obj.save() + prev_obj.save() + self.message_user(request, f"成功将 {obj} 上移") + else: + # 已经是第一个,或者前面没有更小的 order + pass + return redirect(request.META.get('HTTP_REFERER', '..')) + + def move_down_view(self, request, object_id): + obj = self.get_object(request, object_id) + if obj: + # 找到排在它后面的一个 (order 大于它的最小值) + next_obj = self.model.objects.filter(order__gt=obj.order).order_by('order').first() + if next_obj: + # 交换 + obj.order, next_obj.order = next_obj.order, obj.order + obj.save() + next_obj.save() + self.message_user(request, f"成功将 {obj} 下移") + return redirect(request.META.get('HTTP_REFERER', '..')) + + def order_actions(self, obj): + # 只有专家用户才显示排序按钮 + if not getattr(obj, 'is_star', True): # 默认为True是为了兼容其他模型,WeChatUser有is_star字段 + return "默认排序" + + # 使用 inline style 实现基本样式 + btn_style = ( + "display: inline-flex; align-items: center; justify-content: center; " + "width: 26px; height: 26px; border-radius: 6px; " + "background-color: #f3f4f6; color: #4b5563; text-decoration: none; " + "border: 1px solid #e5e7eb; transition: all 0.2s;" + ) + # onmouseover js + hover_js = "this.style.backgroundColor='#dbeafe'; this.style.color='#2563eb'; this.style.borderColor='#bfdbfe';" + out_js = "this.style.backgroundColor='#f3f4f6'; this.style.color='#4b5563'; this.style.borderColor='#e5e7eb';" + + return format_html( + '
' + '' + '' + '' + '{}' + '' + '' + '' + '
', + reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_move_up', args=[obj.pk]), + btn_style, hover_js, out_js, + obj.order, + reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_move_down', args=[obj.pk]), + btn_style, hover_js, out_js, + ) + order_actions.short_description = "排序调节" + order_actions.allow_tags = True + @admin.register(Activity) class ActivityAdmin(ModelAdmin): list_display = ('title', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at') @@ -115,12 +194,24 @@ class ActivitySignupAdmin(ModelAdmin): return "-" @admin.register(Topic) -class TopicAdmin(ModelAdmin): - list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at') +class TopicAdmin(OrderableAdminMixin, ModelAdmin): + list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at', 'order_actions') list_filter = ('category', 'is_pinned', 'created_at', 'related_product', 'related_service', 'related_course') search_fields = ('title', 'content', 'author__nickname') autocomplete_fields = ['author', 'related_product', 'related_service', 'related_course'] inlines = [TopicMediaInline, ReplyInline] + actions = ['reset_ordering'] + + @admin.action(description="重置排序 (按ID顺序)") + def reset_ordering(self, request, queryset): + """ + 将所有帖子按ID顺序重新分配order值 + """ + all_objects = Topic.objects.all().order_by('id') + for index, obj in enumerate(all_objects, start=1): + obj.order = index + obj.save(update_fields=['order']) + self.message_user(request, f"成功重置了 {all_objects.count()} 个帖子的排序权重。") fieldsets = ( ('帖子内容', { diff --git a/backend/community/migrations/0014_alter_topic_options_topic_order.py b/backend/community/migrations/0014_alter_topic_options_topic_order.py new file mode 100644 index 0000000..9b05992 --- /dev/null +++ b/backend/community/migrations/0014_alter_topic_options_topic_order.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.1 on 2026-02-24 16:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('community', '0013_alter_reply_options_reply_is_pinned'), + ] + + operations = [ + migrations.AlterModelOptions( + name='topic', + options={'ordering': ['order', '-is_pinned', '-created_at'], 'verbose_name': '论坛帖子', 'verbose_name_plural': '论坛帖子管理'}, + ), + migrations.AddField( + model_name='topic', + name='order', + field=models.IntegerField(default=0, verbose_name='排序'), + ), + ] diff --git a/backend/community/models.py b/backend/community/models.py index e1cf56e..f08d356 100644 --- a/backend/community/models.py +++ b/backend/community/models.py @@ -139,6 +139,14 @@ class Topic(models.Model): is_pinned = models.BooleanField(default=False, verbose_name="置顶") created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + order = models.IntegerField(default=0, verbose_name="排序") + + def save(self, *args, **kwargs): + is_new = self.pk is None + super().save(*args, **kwargs) + if is_new and getattr(self, 'order', 0) == 0: + Topic.objects.filter(pk=self.pk).update(order=self.pk) + self.order = self.pk def __str__(self): return self.title @@ -175,7 +183,7 @@ class Topic(models.Model): class Meta: verbose_name = "论坛帖子" verbose_name_plural = "论坛帖子管理" - ordering = ['-is_pinned', '-created_at'] + ordering = ['order', '-is_pinned', '-created_at'] class Reply(models.Model): diff --git a/backend/config/settings.py b/backend/config/settings.py index 664f04c..954dc43 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -101,19 +101,7 @@ DATABASES = { #从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。 -# DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') -# if DB_HOST: -# DATABASES['default'] = { -# 'ENGINE': 'django.db.backends.postgresql', -# 'NAME': os.environ.get('DB_NAME', 'market'), -# 'USER': os.environ.get('DB_USER', 'market'), -# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), -# 'HOST': DB_HOST, -# 'PORT': os.environ.get('DB_PORT', '5432'), -# } - - -DB_HOST = os.environ.get('DB_HOST', '121.43.104.161') +DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') if DB_HOST: DATABASES['default'] = { 'ENGINE': 'django.db.backends.postgresql', @@ -121,10 +109,22 @@ if DB_HOST: 'USER': os.environ.get('DB_USER', 'market'), 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), 'HOST': DB_HOST, - 'PORT': os.environ.get('DB_PORT', '6433'), + 'PORT': os.environ.get('DB_PORT', '5432'), } +# DB_HOST = os.environ.get('DB_HOST', '121.43.104.161') +# if DB_HOST: +# DATABASES['default'] = { +# 'ENGINE': 'django.db.backends.postgresql', +# 'NAME': os.environ.get('DB_NAME', 'market'), +# 'USER': os.environ.get('DB_USER', 'market'), +# 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), +# 'HOST': DB_HOST, +# 'PORT': os.environ.get('DB_PORT', '6433'), +# } + + # Password validation # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators