This commit is contained in:
@@ -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('<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)
|
||||
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 = (
|
||||
('帖子内容', {
|
||||
|
||||
@@ -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='排序'),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user