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

如何在Django模板中正确实现树形递归评论?

问题:树形评论区重复显示且无法构建层级

我想在网站里实现树形结构的评论区,但目前评论重复展示,也无法形成正确的树形层级。模型功能应该没问题,推测是模板逻辑出错了。以下是相关代码和问题说明:

当前问题表现

评论重复渲染,树形层级不生效。


主视图代码(Views.py)

class ProfileDetail(DetailView):
    context_object_name = 'profile'
    template_name = 'user_detail.html'

    def get(self, request, *args, **kwargs):
        user_profile = User.objects.get(slug=kwargs.get('slug'))
        settings = Settings.objects.get(user=user_profile.id)
        posts = Post.objects.filter(author=user_profile.id).order_by('-post_date')
        subscriptions = Subscription.objects.filter(author=user_profile.id).order_by('price')
        comment_form = CommentForm()
        return render(request, self.template_name,
                      context={
                          'profile': user_profile,
                          'posts': posts,
                          'settings': settings,
                          'subscriptions': subscriptions,
                          'form': comment_form
                      })

    def post(self, request, *args, **kwargs):
        user_profile = User.objects.get(slug=kwargs.get('slug'))

        if 'subscruption_action' in request.POST:
            action = request.POST['subscruption_action']
            if action == 'unfollow':
                request.user.follows.remove(user_profile.id)
            else:
                request.user.follows.add(user_profile.id)
            request.user.save()
            return HttpResponseRedirect(reverse('profile_detail', kwargs={'slug': user_profile.slug}))


    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        self.object = self.get_object()
        context['posts'] = Post.objects.filter(author=self.object.id)
        context['settings'] = Settings.objects.get(user=self.object.id)
        return context

新增评论视图函数

def add_comment(request, slug, pk):
    form = CommentForm(request.POST)
    post = get_object_or_404(Post, pk=pk)
    if form.is_valid():
        cleaned_data = form.cleaned_data
        try:
            answered_comment = Comment.objects.get(pk=cleaned_data.get('parent_comment'))
        except ObjectDoesNotExist:
            new_comment = Comment(
                user=request.user,
                post=post,
                body=cleaned_data.get('body'),
            )
            new_comment.save()
        else:
            new_comment = Comment(
                user=request.user,
                post=post,
                body=cleaned_data.get('body'),
                comment_to_reply=answered_comment
            )
            new_comment.save()
    else:
        messages.error(request, 'Form is not valid!')
    return redirect('index')

评论模型(Models.py)

class Comment(models.Model):
    user = models.ForeignKey(User, related_name='user', on_delete=models.CASCADE)
    post = models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE)
    body = models.TextField(max_length=255)
    comment_to_reply = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
    created_at = models.DateTimeField(auto_now_add=True)

评论模板(comments_template.html)

{% if post.comments %}
    {% for comment in post.comments.all %}
        <a name="comment-{{ comment.id }}"></a>
        <div class="row" id="{{ comment.id }}">
            <div class="shadow-sm mb-3 card border-0 p-3 bg-light">
                <div class="panel-heading">
                    <strong>{{ comment.user.username }}</strong>
                    <a href="#comment-{{ comment.id }}"></a>
                    <small class="text-muted">{{ comment.created_at }}</small>
                </div>
                {% if form %}
                <div class="row">
                    <div class="col-10">{{ comment.body }}</div>
                    <div class="col text-end">
                        <a class="btn btn-primary btn-sm" onclick="return show_comments_form({{ comment.id }})">Reply</a>
                    </div>
                </div>
                {% else %}
                <div>{{ comment.body }}</div>
                {% endif %}
            </div>
        </div>
        {% if comment.replies %}
            <div class="offset-md-3">
            {% for comment in comment.replies.all %}
            <div class="row">
                <div class="col offset-md-1">
                    <div class="shadow-sm mb-3 border-0 p-3 bg-light" id="{{ comment.id }}">
                        {% if form %}
                            <div class="row">
                                <strong><a href="{% url 'profile_detail' comment.user.slug %}">{{ comment.user.username }}</a></strong>
                                <small class="text-muted">{{ comment.created_at }}</small>
                                <p>{{ comment.body }}</p>
                                <div class="col">
                                    <small class="text-muted">Reply to - {{ comment.comment_to_reply.user }}</small>
                                </div>
                                <div class="col-2 text-end">
                                    <a class="btn btn-primary btn-sm" onclick="return show_comments_form({{ comment.id }})">Reply</a>
                                </div>
                            </div>
                        {% else %}
                            <strong><a href="{% url 'profile_detail' reply.user.slug %}">{{ comment.user.username }}</a></strong>
                            <small class="text-muted">{{ reply.created_at }}</small>
                            <p>{{ reply.body }}</p>
                            <small class="text-muted">Reply to - {{ comment.comment_to_reply.user }}</small>
                        {% endif %}
                    </div>
                </div>
            </div>
        {% endfor %}
            </div>
        {% endif %}
    {% endfor %}

问题分析与修复方案

核心问题1:根评论未过滤,导致重复渲染

post.comments.all会取出该帖子下所有评论,包括作为回复的子评论(即comment_to_reply不为空的评论)。这就导致子评论既会在根循环里被渲染一次,又会在父评论的replies循环里被渲染一次,造成重复显示。

修复:只渲染无父级的根评论

把模板中的根评论循环修改为:

{% for comment in post.comments.filter(comment_to_reply__isnull=True) %}

也可以在视图层提前处理,给每个Post添加根评论的查询集,避免在模板中写查询逻辑。

核心问题2:循环变量名重复,覆盖父级变量

子评论循环中使用了{% for comment in comment.replies.all %},这里的comment变量和外层循环的comment重名,会覆盖父级变量,引发逻辑混乱,后续如果要支持多级回复,问题会更严重。

修复:修改子循环变量名

将内层循环的变量改为reply(或其他不重复的名称),并同步修改内层模板中的所有引用:

{% if comment.replies %}
    <div class="offset-md-3">
    {% for reply in comment.replies.all %}
    <div class="row">
        <div class="col offset-md-1">
            <div class="shadow-sm mb-3 border-0 p-3 bg-light" id="{{ reply.id }}">
                {% if form %}
                    <div class="row">
                        <strong><a href="{% url 'profile_detail' reply.user.slug %}">{{ reply.user.username }}</a></strong>
                        <small class="text-muted">{{ reply.created_at }}</small>
                        <p>{{ reply.body }}</p>
                        <div class="col">
                            <small class="text-muted">Reply to - {{ reply.comment_to_reply.user }}</small>
                        </div>
                        <div class="col-2 text-end">
                            <a class="btn btn-primary btn-sm" onclick="return show_comments_form({{ reply.id }})">Reply</a>
                        </div>
                    </div>
                {% else %}
                    <strong><a href="{% url 'profile_detail' reply.user.slug %}">{{ reply.user.username }}</a></strong>
                    <small class="text-muted">{{ reply.created_at }}</small>
                    <p>{{ reply.body }}</p>
                    <small class="text-muted">Reply to - {{ reply.comment_to_reply.user }}</small>
                {% endif %}
            </div>
        </div>
    </div>
{% endfor %}
    </div>
{% endif %}

额外问题:模板else分支变量错误

原模板{% else %}分支中使用了未定义的reply变量,修复后统一改为reply即可解决报错。

优化建议:支持多级回复

如果需要支持更深层级的回复(比如回复的回复),可以把评论渲染逻辑抽成模板片段,通过递归调用实现:

  1. 创建comment_item.html模板片段:
<div class="row" id="{{ comment.id }}">
    <div class="shadow-sm mb-3 card border-0 p-3 bg-light">
        <div class="panel-heading">
            <strong>{{ comment.user.username }}</strong>
            <a href="#comment-{{ comment.id }}"></a>
            <small class="text-muted">{{ comment.created_at }}</small>
        </div>
        {% if form %}
        <div class="row">
            <div class="col-10">{{ comment.body }}</div>
            <div class="col text-end">
                <a class="btn btn-primary btn-sm" onclick="return show_comments_form({{ comment.id }})">Reply</a>
            </div>
        </div>
        {% else %}
        <div>{{ comment.body }}</div>
        {% endif %}
    </div>
</div>
{% if comment.replies.exists %}
    <div class="offset-md-3">
        {% for reply in comment.replies.all %}
            {% include 'comment_item.html' with comment=reply %}
        {% endfor %}
    </div>
{% endif %}
  1. 在主模板中调用:
{% if post.comments %}
    {% for comment in post.comments.filter(comment_to_reply__isnull=True) %}
        {% include 'comment_item.html' with comment=comment %}
    {% endfor %}
{% endif %}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 00:37:03