diff --git a/backend/requirements.txt b/backend/requirements.txt index 78ee9bc..3515011 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -21,4 +21,5 @@ drf-spectacular-sidecar==2026.1.1 gunicorn==21.2.0 requests django-filter +django-admin-sortable2 diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 45e3192..6254b7b 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -4,6 +4,7 @@ from django.db.models import Sum from django import forms from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display +from adminsortable2.admin import SortableAdminMixin from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment import qrcode from io import BytesIO @@ -83,9 +84,9 @@ class WeChatPayConfigAdmin(ModelAdmin): ) @admin.register(ESP32Config) -class ESP32ConfigAdmin(ModelAdmin): +class ESP32ConfigAdmin(SortableAdminMixin, ModelAdmin): form = ESP32ConfigAdminForm - list_display = ('name', 'chip_type', 'price', 'stock', 'has_camera', 'has_microphone') + list_display = ('name', 'chip_type', 'price', 'stock', 'has_camera', 'has_microphone', 'order') list_filter = ('chip_type', 'has_camera') search_fields = ('name', 'description') inlines = [ProductFeatureInline] @@ -107,8 +108,8 @@ class ESP32ConfigAdmin(ModelAdmin): ) @admin.register(Service) -class ServiceAdmin(ModelAdmin): - list_display = ('title', 'created_at') +class ServiceAdmin(SortableAdminMixin, ModelAdmin): + list_display = ('title', 'created_at', 'order') search_fields = ('title', 'description') fieldsets = ( ('基本信息', { @@ -150,8 +151,8 @@ class ServiceOrderAdmin(ModelAdmin): ) @admin.register(VCCourse) -class VCCourseAdmin(ModelAdmin): - list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at') +class VCCourseAdmin(SortableAdminMixin, ModelAdmin): + list_display = ('title', 'course_type', 'price', 'tag', 'instructor', 'lesson_count', 'duration', 'created_at', 'order') search_fields = ('title', 'description', 'instructor', 'tag') list_filter = ('course_type', 'instructor', 'tag') fieldsets = ( diff --git a/backend/shop/migrations/0030_alter_esp32config_options_alter_service_options_and_more.py b/backend/shop/migrations/0030_alter_esp32config_options_alter_service_options_and_more.py new file mode 100644 index 0000000..9b74e1c --- /dev/null +++ b/backend/shop/migrations/0030_alter_esp32config_options_alter_service_options_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 6.0.1 on 2026-02-13 16:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0029_fix_legacy_fields'), + ] + + operations = [ + migrations.AlterModelOptions( + name='esp32config', + options={'ordering': ['order'], 'verbose_name': '硬件配置 (小智参数)', 'verbose_name_plural': '硬件配置 (小智参数)'}, + ), + migrations.AlterModelOptions( + name='service', + options={'ordering': ['order'], 'verbose_name': 'AI服务', 'verbose_name_plural': 'AI服务管理'}, + ), + migrations.AlterModelOptions( + name='vccourse', + options={'ordering': ['order'], 'verbose_name': 'VC课程', 'verbose_name_plural': 'VC课程管理'}, + ), + migrations.AddField( + model_name='esp32config', + name='order', + field=models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重'), + ), + migrations.AddField( + model_name='service', + name='order', + field=models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重'), + ), + migrations.AddField( + model_name='vccourse', + name='order', + field=models.IntegerField(default=0, help_text='数字越小越靠前', verbose_name='排序权重'), + ), + migrations.AlterField( + model_name='order', + name='config', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='shop.esp32config', verbose_name='所选配置'), + ), + ] diff --git a/backend/shop/models.py b/backend/shop/models.py index d0a30c0..0c739b8 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -109,6 +109,7 @@ class ESP32Config(models.Model): detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用URL") static_image_url = models.URLField(blank=True, null=True, verbose_name="产品静态图 (URL)") model_3d_url = models.URLField(blank=True, null=True, verbose_name="产品3D模型 (URL)") + order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前") def __str__(self): return f"{self.name} - ¥{self.price}" @@ -116,6 +117,7 @@ class ESP32Config(models.Model): class Meta: verbose_name = "硬件配置 (小智参数)" verbose_name_plural = "硬件配置 (小智参数)" + ordering = ['order'] class ProductFeature(models.Model): @@ -226,7 +228,7 @@ class Order(models.Model): ('cancelled', '已取消'), ) - config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置", null=True, blank=True) + config = models.ForeignKey(ESP32Config, on_delete=models.CASCADE, verbose_name="所选配置", null=True, blank=True, related_name='orders') course = models.ForeignKey('VCCourse', on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所选课程", related_name='orders') quantity = models.IntegerField(default=1, verbose_name="数量") total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总价") @@ -279,6 +281,7 @@ class Service(models.Model): detail_image = models.ImageField(upload_to='services/details/', blank=True, null=True, verbose_name="详情页长图 (上传)") detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前") def __str__(self): return self.title @@ -286,6 +289,7 @@ class Service(models.Model): class Meta: verbose_name = "AI服务" verbose_name_plural = "AI服务管理" + ordering = ['order'] class ServiceOrder(models.Model): @@ -354,6 +358,7 @@ class VCCourse(models.Model): detail_image_url = models.URLField(blank=True, null=True, verbose_name="详情页长图 (URL)", help_text="如果填写了URL,将优先使用URL") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + order = models.IntegerField(default=0, verbose_name="排序权重", help_text="数字越小越靠前") def __str__(self): return self.title @@ -361,6 +366,7 @@ class VCCourse(models.Model): class Meta: verbose_name = "VC课程" verbose_name_plural = "VC课程管理" + ordering = ['order'] class CourseEnrollment(models.Model): diff --git a/backend/shop/views.py b/backend/shop/views.py index 85f75af..fd9d1b6 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -604,7 +604,7 @@ class VCCourseViewSet(viewsets.ReadOnlyModelViewSet): """ VC课程列表和详情 """ - queryset = VCCourse.objects.all().order_by('-created_at') + queryset = VCCourse.objects.all() serializer_class = VCCourseSerializer class CourseEnrollmentViewSet(viewsets.ModelViewSet): @@ -628,7 +628,7 @@ class ServiceViewSet(viewsets.ReadOnlyModelViewSet): """ AI服务列表和详情 """ - queryset = Service.objects.all().order_by('-created_at') + queryset = Service.objects.all() serializer_class = ServiceSerializer class ServiceOrderViewSet(viewsets.ModelViewSet): diff --git a/frontend/src/pages/ServiceDetail.jsx b/frontend/src/pages/ServiceDetail.jsx index 567ff27..66e69f9 100644 --- a/frontend/src/pages/ServiceDetail.jsx +++ b/frontend/src/pages/ServiceDetail.jsx @@ -186,7 +186,10 @@ const ServiceDetail = () => { border: `1px solid ${service.color}66`, borderRadius: '6px', fontSize: '14px', - backdropFilter: 'blur(4px)' + backdropFilter: 'blur(4px)', + whiteSpace: 'normal', + height: 'auto', + textAlign: 'left' }} > {feat}