You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Django中根据主下拉框值筛选子下拉框选项

Django 实现主类别选择后联动过滤子类别下拉框

我有三个Django模型:MainCategory、SubCategory和Record。SubCategory通过外键关联MainCategory,Record同时关联主类别和子类别。需求是在Record表单中选择主类别下拉框后,子类别下拉框只显示对应主类别的选项。


现有模型代码

from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator

class MainCategory(models.Model):
    name = models.CharField(max_length=100, unique=True)
    activity_status = models.BooleanField()

    def __str__(self):
        return self.name


class SubCategory(models.Model):
    name = models.CharField(max_length=100)
    main_category = models.ForeignKey(MainCategory, on_delete=models.CASCADE)
    activity_status = models.BooleanField()

    def __str__(self):
        return self.name

class Record(models.Model):
    RECORD_TYPES = [
        ('income', 'Income'),
        ('expense', 'Expense'),
        ('transfer', 'Transfer'),
    ]

    DELETED_CATEGORY = 'Deleted category'

    date = models.DateField(default=timezone.now)
    category = models.ForeignKey(MainCategory, on_delete=models.SET(DELETED_CATEGORY), default=1)
    subcategory = models.ForeignKey(SubCategory, on_delete=models.SET(DELETED_CATEGORY))
    type = models.CharField(max_length=10, choices=RECORD_TYPES)
    amount = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(0)])
    account = models.ForeignKey(Account, on_delete=models.CASCADE)
    notes = models.TextField(blank=True)
    transfer_ops = models.ForeignKey(TransferOperation, on_delete=models.CASCADE, null=True, blank=True)
    activity_status = models.BooleanField()

    def __str__(self):
        return f"{self.date} - {self.type}: {self.amount}"

现有视图代码

from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Record, SubCategory
from .forms import RecordForm

class CreateRecordView(CreateView):
    model = Record
    form_class = RecordForm
    template_name = 'create_record.html'
    success_url = reverse_lazy('home')

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        form.fields['subcategory'].queryset = SubCategory.objects.none()

        category_id = self.request.POST.get('category') or self.request.GET.get('category')
        if category_id:
            form.fields['subcategory'].queryset = SubCategory.objects.filter(main_category_id=category_id)

        return form

尝试过的模板及JS代码(未生效)

模板代码

{% extends 'base.html' %}

{% block content %}
<h2>Create Record</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="form-group">
  <label for="id_category">Category</label>
  {{ form.category }}
</div>
<div class="form-group">
  <label for="id_subcategory">Subcategory</label>
  <select name="subcategory" id="id_subcategory" class="form-control">
    <option value="">---------</option>
    {% for subcategory in subcategories %}
      <option value="{{ subcategory.pk }}" data-category="{{ subcategory.main_category.pk }}">{{ subcategory.name }}</option>
    {% endfor %}
  </select>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>

<script src="C:\Users\rere\PycharmProjects\bocket\js\filter_subcategories.js"> 
</script>
{% endblock %}

JS代码

$(document).ready(function() {
var subcategorySelect = $('#id_subcategory');
var subcategoryOptions = subcategorySelect.find('option');

$('#id_category').change(function() {
var selectedCategoryId = $(this).val();

// Reset subcategory options
subcategorySelect.val('');
subcategoryOptions.hide();

// Show subcategories corresponding to the selected category
subcategoryOptions.filter(function() {
  return $(this).data('category') == selectedCategoryId;
}).show();
});
});

问题分析及修复方案

核心问题

  1. 模板重复渲染表单字段:{{ form.as_p }}已经包含了category和subcategory,后续手动写的输入框会导致页面存在两组控件,JS绑定的可能不是表单实际提交的控件
  2. JS操作option的hide()/show()存在浏览器兼容性问题,无法正常生效
  3. 模板中使用本地绝对路径引入JS文件,不符合Django静态文件规范
  4. 视图未传递subcategories变量到模板,导致子类别选项无法渲染

修复步骤

1. 修正视图:传递子类别数据+完善表单逻辑

class CreateRecordView(CreateView):
    model = Record
    form_class = RecordForm
    template_name = 'create_record.html'
    success_url = reverse_lazy('home')

    def get_form(self, form_class=None):
        form = super().get_form(form_class)
        # 根据传入的category_id过滤子类别,兼容页面刷新/表单提交场景
        category_id = self.request.POST.get('category') or self.request.GET.get('category')
        if category_id:
            form.fields['subcategory'].queryset = SubCategory.objects.filter(main_category_id=category_id, activity_status=True)
        else:
            form.fields['subcategory'].queryset = SubCategory.objects.none()
        return form

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 传递所有激活状态的子类别到模板,供JS过滤使用
        context['all_subcategories'] = SubCategory.objects.filter(activity_status=True)
        return context

2. 修正模板:移除重复字段+规范静态文件引入

{% extends 'base.html' %}
{% load static %}

{% block content %}
<h2>Create Record</h2>
<form method="post">
    {% csrf_token %}
    <!-- 主类别字段 -->
    <div class="form-group">
        <label for="{{ form.category.id_for_label }}">Category</label>
        {{ form.category }}
    </div>
    <!-- 子类别字段 -->
    <div class="form-group">
        <label for="{{ form.subcategory.id_for_label }}">Subcategory</label>
        <select name="{{ form.subcategory.name }}" id="{{ form.subcategory.id_for_label }}" class="form-control">
            <option value="">---------</option>
            {% for subcategory in all_subcategories %}
                <option value="{{ subcategory.pk }}" data-category="{{ subcategory.main_category.pk }}">{{ subcategory.name }}</option>
            {% endfor %}
        </select>
    </div>
    <!-- 渲染表单其他字段 -->
    <div class="form-group">
        <label for="{{ form.type.id_for_label }}">Type</label>
        {{ form.type }}
    </div>
    <div class="form-group">
        <label for="{{ form.amount.id_for_label }}">Amount</label>
        {{ form.amount }}
    </div>
    <div class="form-group">
        <label for="{{ form.account.id_for_label }}">Account</label>
        {{ form.account }}
    </div>
    <div class="form-group">
        <label for="{{ form.notes.id_for_label }}">Notes</label>
        {{ form.notes }}
    </div>
    <button type="submit" class="btn btn-primary">Save</button>
</form>

<script src="{% static 'js/filter_subcategories.js' %}"></script>
{% endblock %}

3. 修正JS代码:改用重新构建选项的方式

$(document).ready(function() {
    // 保存所有子类别选项的副本
    const allSubcategoryOptions = $('#id_subcategory option').clone();
    
    $('#id_category').change(function() {
        const selectedCategoryId = $(this).val();
        const subcategorySelect = $('#id_subcategory');
        
        // 清空现有选项,保留默认空选项
        subcategorySelect.empty().append('<option value="">---------</option>');
        
        if (!selectedCategoryId) {
            return;
        }
        
        // 筛选对应类别的子类别并添加到下拉框
        allSubcategoryOptions.each(function() {
            const option = $(this);
            if (option.data('category') == selectedCategoryId) {
                subcategorySelect.append(option.clone());
            }
        });
    });
});

4. 配置静态文件(若未配置)

在项目settings.py中添加静态文件配置:

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    BASE_DIR / "static",
]

将filter_subcategories.js放在项目根目录的static/js/文件夹下。


内容的提问来源于stack exchange,提问作者Mohammed Ahmed Mohammed

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 14:24:54