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

如何在Django的ExpenseForm中实现分类选择依赖日期?

实现家庭预算应用中分类选择依赖日期的方案

问题背景

我正在开发一款家庭预算管理应用,包含Date、Expense、Category、Income等模型,模型代码如下:

class Date(models.Model):
    name = models.CharField(max_length = 15)    
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='date')

class Category(models.Model):
    name = models.CharField(max_length=100)
    budget = models.DecimalField(max_digits=8, decimal_places=2)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='category')
    date = models.ForeignKey(Date, on_delete=models.CASCADE, related_name='date', default='Styczeń 2022')

class Expense(models.Model):
    name = models.CharField(max_length=100)
    amount = models.DecimalField(max_digits=8, decimal_places=2)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="account")
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense')
    date = models.ForeignKey(Date, on_delete=models.CASCADE, default='Styczeń 2022')

class Income(models.Model):
    name = models.CharField(max_length=100)
    amount = models.DecimalField(max_digits=8, decimal_places=2)
    account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="account_income")
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='income')

每个Category都关联对应的Date,我需要在ExpenseForm中实现分类选择依赖于所选日期,即仅显示选中日期对应的分类。这是因为用户可能存在同名但不同日期的分类(如2022年1月、2月、3月的“食品”分类),若不做过滤会导致选择混淆。

当前我的ExpenseForm代码如下:

class ExpenseForm(forms.ModelForm):
    class Meta:
        model = Expense
        fields = ('name', 'amount', 'category', 'account', 'date',)

    def __init__(self, user, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['category'] = forms.ModelChoiceField(queryset=user.category.all())
        self.fields['account'] = forms.ModelChoiceField(queryset=user.account.all())
        self.fields['date'] = forms.ModelChoiceField(queryset=user.date.all())
        self.fields['name'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'})
        self.fields['amount'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'})
        self.fields['category'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'})
        self.fields['account'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'})
        self.fields['date'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'})

        self.fields['name'].label = 'Nazwa'
        self.fields['amount'].label = 'Kwota'
        self.fields['category'].label = 'Kategoria'
        self.fields['account'].label = 'Konto bankowe'
        self.fields['date'].label = 'Zakres dat'

可行实现思路

思路一:前端AJAX动态过滤(无刷新体验)

这是用户体验最优的方案,通过JavaScript监听日期选择变化,异步请求对应分类并更新下拉选项:

  1. 调整表单初始化逻辑
    修改ExpenseForm的__init__方法,根据场景(新增/编辑)初始化分类查询集:

    def __init__(self, user, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 初始化账户和日期字段
        self.fields['account'] = forms.ModelChoiceField(queryset=user.account.all())
        self.fields['date'] = forms.ModelChoiceField(queryset=user.date.all())
    
        # 初始化分类查询集:编辑时用实例关联的日期,新增时用用户第一个日期
        if self.instance.pk:
            selected_date = self.instance.date
            self.fields['category'] = forms.ModelChoiceField(
                queryset=user.category.filter(date=selected_date)
            )
        else:
            default_date = user.date.first()
            self.fields['category'] = forms.ModelChoiceField(
                queryset=user.category.filter(date=default_date) if default_date else user.category.none()
            )
    
        # 保留原有样式和标签设置...
    
  2. 给表单元素添加ID
    在日期和分类字段的widget属性中添加唯一ID,方便JS定位:

    self.fields['date'].widget.attrs.update({
        # 原有class...
        'id': 'id_expense_date'
    })
    self.fields['category'].widget.attrs.update({
        # 原有class...
        'id': 'id_expense_category'
    })
    
  3. 添加前端JS逻辑
    在渲染表单的模板中加入以下脚本,实现动态更新分类选项:

    const dateSelect = document.getElementById('id_expense_date');
    const categorySelect = document.getElementById('id_expense_category');
    
    dateSelect.addEventListener('change', function() {
        const dateId = this.value;
        // 清空现有分类选项
        categorySelect.innerHTML = '<option value="">请选择分类</option>';
    
        if (!dateId) return;
    
        // 发送AJAX请求获取对应日期的分类
        fetch(`/get_categories/${dateId}/`)
            .then(response => response.json())
            .then(data => {
                data.forEach(category => {
                    const option = document.createElement('option');
                    option.value = category.id;
                    option.textContent = category.name;
                    categorySelect.appendChild(option);
                });
            })
            .catch(error => console.error('获取分类失败:', error));
    });
    
  4. 后端添加AJAX接口
    在views.py中新增处理分类请求的视图:

    from django.http import JsonResponse
    from django.contrib.auth.decorators import login_required
    from .models import Category
    
    @login_required
    def get_categories(request, date_id):
        # 仅返回当前用户对应日期的分类
        categories = Category.objects.filter(date_id=date_id, user=request.user)
        category_list = [{'id': cat.id, 'name': cat.name} for cat in categories]
        return JsonResponse(category_list, safe=False)
    
  5. 配置路由
    在urls.py中添加对应路由:

    path('get_categories/<int:date_id>/', views.get_categories, name='get_categories'),
    

思路二:表单提交刷新(简单直接)

如果不需要无刷新体验,这种方案实现成本更低,通过页面刷新来更新分类选项:

  1. 修改视图逻辑
    在处理表单的视图中,根据GET参数中的日期ID过滤分类:

    def expense_form(request):
        user = request.user
        selected_date_id = request.GET.get('date_id')
        form = ExpenseForm(user)
    
        if selected_date_id:
            try:
                selected_date = user.date.get(id=selected_date_id)
                form.fields['category'].queryset = user.category.filter(date=selected_date)
                # 同时设置日期字段的初始值
                form.fields['date'].initial = selected_date_id
            except Date.DoesNotExist:
                pass
    
        if request.method == 'POST':
            # 处理表单提交逻辑...
            form = ExpenseForm(user, request.POST)
            if form.is_valid():
                form.save()
                # 重定向或返回成功页面
    
        return render(request, 'expense_form.html', {'form': form})
    
  2. 模板中添加自动提交逻辑
    在模板中给日期选择框添加onchange事件,触发表单提交:

    <form method="get">
        {{ form.date }}
        <input type="submit" style="display: none;" id="date_submit_btn">
    </form>
    
    <script>
        document.getElementById('id_expense_date').addEventListener('change', function() {
            document.getElementById('date_submit_btn').click();
        });
    </script>
    

    注意:这种方式会刷新整个页面,适合对交互要求不高的场景。

额外优化点

  • 重写Date模型的__str__方法:让日期下拉选项显示更友好
    class Date(models.Model):
        # 现有字段...
        def __str__(self):
            return self.name
    
  • 表单验证增强:确保提交的分类确实属于所选日期,避免前端绕过验证的情况
    class ExpenseForm(forms.ModelForm):
        # 现有代码...
        def clean(self):
            cleaned_data = super().clean()
            date = cleaned_data.get('date')
            category = cleaned_data.get('category')
    
            if date and category:
                if category.date != date:
                    raise forms.ValidationError("所选分类不属于当前日期")
            return cleaned_data
    

内容的提问来源于stack exchange,提问作者Filip Kania

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 22:01:27