排序
All checks were successful
Deploy to Server / deploy (push) Successful in 26s

This commit is contained in:
jeremygan2021
2026-02-25 00:47:50 +08:00
parent 96c12b9e58
commit 05299060dc
4 changed files with 138 additions and 17 deletions

View File

@@ -1,5 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html 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.admin import ModelAdmin, TabularInline
from unfold.decorators import display from unfold.decorators import display
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
@@ -28,6 +30,83 @@ class TopicMediaInline(TabularInline):
readonly_fields = ('created_at',) readonly_fields = ('created_at',)
can_delete = True can_delete = True
class OrderableAdminMixin:
"""
为 Admin 添加排序功能的 Mixin
提供上移、下移按钮,直接交换 order 值
"""
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('<path:object_id>/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('<path:object_id>/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(
'<div style="display: flex; align-items: center; gap: 6px;">'
'<a href="{}" title="上移" style="{}" onmouseover="{}" onmouseout="{}">'
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 15l-6-6-6 6"/></svg>'
'</a>'
'<span style="font-weight: 700; font-family: system-ui, -apple-system, sans-serif; min-width: 20px; text-align: center; color: #374151; font-size: 13px;">{}</span>'
'<a href="{}" title="下移" style="{}" onmouseover="{}" onmouseout="{}">'
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>'
'</a>'
'</div>',
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) @admin.register(Activity)
class ActivityAdmin(ModelAdmin): class ActivityAdmin(ModelAdmin):
list_display = ('title', 'banner_display', 'start_time', 'location', 'signup_count', 'is_visible', 'is_active', 'auto_confirm', 'created_at') 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 "-" return "-"
@admin.register(Topic) @admin.register(Topic)
class TopicAdmin(ModelAdmin): class TopicAdmin(OrderableAdminMixin, ModelAdmin):
list_display = ('title', 'category', 'author', 'get_related_item', 'reply_count', 'view_count', 'is_pinned', 'created_at') 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') list_filter = ('category', 'is_pinned', 'created_at', 'related_product', 'related_service', 'related_course')
search_fields = ('title', 'content', 'author__nickname') search_fields = ('title', 'content', 'author__nickname')
autocomplete_fields = ['author', 'related_product', 'related_service', 'related_course'] autocomplete_fields = ['author', 'related_product', 'related_service', 'related_course']
inlines = [TopicMediaInline, ReplyInline] 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 = ( fieldsets = (
('帖子内容', { ('帖子内容', {

View File

@@ -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='排序'),
),
]

View File

@@ -139,6 +139,14 @@ class Topic(models.Model):
is_pinned = models.BooleanField(default=False, verbose_name="置顶") is_pinned = models.BooleanField(default=False, verbose_name="置顶")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间") created_at = models.DateTimeField(auto_now_add=True, verbose_name="发布时间")
updated_at = models.DateTimeField(auto_now=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): def __str__(self):
return self.title return self.title
@@ -175,7 +183,7 @@ class Topic(models.Model):
class Meta: class Meta:
verbose_name = "论坛帖子" verbose_name = "论坛帖子"
verbose_name_plural = "论坛帖子管理" verbose_name_plural = "论坛帖子管理"
ordering = ['-is_pinned', '-created_at'] ordering = ['order', '-is_pinned', '-created_at']
class Reply(models.Model): class Reply(models.Model):

View File

@@ -101,19 +101,7 @@ DATABASES = {
#从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。 #从环境变量获取数据库配置 (Docker 环境会自动注入这些变量。
# DB_HOST = os.environ.get('DB_HOST', '6.6.6.66') 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')
if DB_HOST: if DB_HOST:
DATABASES['default'] = { DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
@@ -121,10 +109,22 @@ if DB_HOST:
'USER': os.environ.get('DB_USER', 'market'), 'USER': os.environ.get('DB_USER', 'market'),
'PASSWORD': os.environ.get('DB_PASSWORD', '123market'), 'PASSWORD': os.environ.get('DB_PASSWORD', '123market'),
'HOST': DB_HOST, '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 # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators