如何在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(); }); });
问题分析及修复方案
核心问题
- 模板重复渲染表单字段:
{{ form.as_p }}已经包含了category和subcategory,后续手动写的输入框会导致页面存在两组控件,JS绑定的可能不是表单实际提交的控件 - JS操作option的
hide()/show()存在浏览器兼容性问题,无法正常生效 - 模板中使用本地绝对路径引入JS文件,不符合Django静态文件规范
- 视图未传递
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
相关产品推荐
相关产品推荐

