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

使用Django+Bootstrap Modal实现外键模型Activity的异步CRUD

Django Bootstrap Modal 异步操作关联模型完整实现方案

1. 模型定义(models.py参考)

from django.db import models

class Dossier(models.Model):
    name = models.CharField(max_length=100)
    # 其他Dossier字段按需添加
    def __str__(self):
        return self.name

class Activity(models.Model):
    dossier = models.ForeignKey(Dossier, on_delete=models.CASCADE, related_name='activities')
    title = models.CharField(max_length=200)
    content = models.TextField()
    create_time = models.DateTimeField(auto_now_add=True)
    # 其他Activity字段按需添加
    def __str__(self):
        return self.title

2. 表单定义(forms.py)

from django import forms
from .models import Activity

class ActivityForm(forms.ModelForm):
    class Meta:
        model = Activity
        fields = ['title', 'content'] # 外键dossier由后端自动赋值,无需放入表单
        widgets = {
            'content': forms.Textarea(attrs={'rows': 3}),
        }

3. 路由配置(urls.py)

from django.urls import path
from . import views

urlpatterns = [
    # 原有Dossier编辑路由
    path('dossier/edit/<int:pk>/', views.dossier_edit, name='dossier_edit'),
    # Activity异步操作专用路由
    path('activity/create/', views.activity_create, name='activity_create'),
    path('activity/<int:pk>/detail/', views.activity_detail, name='activity_detail'),
    path('activity/<int:pk>/update/', views.activity_update, name='activity_update'),
    path('activity/<int:pk>/delete/', views.activity_delete, name='activity_delete'),
]

4. 视图逻辑实现(views.py)

from django.shortcuts import get_object_or_404, render
from django.http import JsonResponse
from .models import Dossier, Activity
from .forms import ActivityForm

# 原有Dossier编辑视图
def dossier_edit(request, pk):
    dossier = get_object_or_404(Dossier, pk=pk)
    activities = dossier.activities.all()
    return render(request, 'dossier_edit.html', {'dossier': dossier, 'activities': activities})

# 新增Activity接口
def activity_create(request):
    if request.method == 'POST' and request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
        form = ActivityForm(request.POST)
        if form.is_valid():
            activity = form.save(commit=False)
            dossier_id = request.POST.get('dossier_id')
            activity.dossier = get_object_or_404(Dossier, pk=dossier_id)
            activity.save()
            return JsonResponse({'status': 'success', 'id': activity.pk, 'title': activity.title, 'content': activity.content})
        else:
            return JsonResponse({'status': 'error', 'errors': form.errors})
    return JsonResponse({'status': 'error', 'msg': '无效请求'})

# 获取单条Activity数据(编辑表单填充用)
def activity_detail(request, pk):
    if request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
        activity = get_object_or_404(Activity, pk=pk)
        return JsonResponse({
            'status': 'success',
            'title': activity.title,
            'content': activity.content
        })
    return JsonResponse({'status': 'error', 'msg': '无效请求'})

# 更新Activity接口
def activity_update(request, pk):
    activity = get_object_or_404(Activity, pk=pk)
    if request.method == 'POST' and request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
        form = ActivityForm(request.POST, instance=activity)
        if form.is_valid():
            form.save()
            return JsonResponse({'status': 'success', 'id': activity.pk, 'title': activity.title, 'content': activity.content})
        else:
            return JsonResponse({'status': 'error', 'errors': form.errors})
    return JsonResponse({'status': 'error', 'msg': '无效请求'})

# 删除Activity接口
def activity_delete(request, pk):
    if request.method == 'POST' and request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest':
        activity = get_object_or_404(Activity, pk=pk)
        activity.delete()
        return JsonResponse({'status': 'success'})
    return JsonResponse({'status': 'error', 'msg': '无效请求'})

5. 前端模板实现(dossier_edit.html)

<!-- 原有Dossier编辑表单代码,必须包含{% csrf_token %}标签 -->
{% csrf_token %}
<hr>
<h3>关联活动记录</h3>
<button type="button" class="btn btn-primary mb-3" id="addActivityBtn" data-dossier-id="{{ dossier.pk }}">
  新增活动
</button>
<div id="activityList">
  {% for activity in activities %}
  <div class="card mb-2 activity-item" data-activity-id="{{ activity.pk }}">
    <div class="card-body d-flex justify-content-between align-items-center">
      <div>
        <h5 class="card-title">{{ activity.title }}</h5>
        <p class="card-text">{{ activity.content }}</p>
      </div>
      <div>
        <button class="btn btn-sm btn-outline-secondary editActivityBtn" data-activity-id="{{ activity.pk }}">编辑</button>
        <button class="btn btn-sm btn-outline-danger deleteActivityBtn" data-activity-id="{{ activity.pk }}">删除</button>
      </div>
    </div>
  </div>
  {% empty %}
  <p>暂无关联活动记录</p>
  {% endfor %}
</div>

<!-- Bootstrap Modal 公共弹窗 -->
<div class="modal fade" id="activityModal" tabindex="-1" aria-labelledby="activityModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="activityModalLabel">新增活动</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <form id="activityForm">
        <div class="modal-body">
          <div class="mb-3">
            <label for="id_title" class="form-label">标题</label>
            <input type="text" class="form-control" id="id_title" name="title" required>
          </div>
          <div class="mb-3">
            <label for="id_content" class="form-label">内容</label>
            <textarea class="form-control" id="id_content" name="content" rows="3" required></textarea>
          </div>
          <div class="text-danger" id="formErrors"></div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
          <button type="submit" class="btn btn-primary" id="submitActivityBtn">保存</button>
        </div>
      </form>
    </div>
  </div>
</div>

6. JS交互逻辑实现

// 全局状态存储
let activityAction = 'create';
let currentActivityId = null;
let currentDossierId = null;
const activityModal = new bootstrap.Modal(document.getElementById('activityModal'));
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;

// 新增按钮绑定事件
document.getElementById('addActivityBtn').addEventListener('click', function() {
  activityAction = 'create';
  currentDossierId = this.dataset.dossierId;
  document.getElementById('activityModalLabel').textContent = '新增活动';
  document.getElementById('activityForm').reset();
  document.getElementById('formErrors').innerHTML = '';
  activityModal.show();
});

// 现有编辑按钮绑定事件
document.querySelectorAll('.editActivityBtn').forEach(btn => {
  btn.addEventListener('click', handleEditClick)
});

// 现有删除按钮绑定事件
document.querySelectorAll('.deleteActivityBtn').forEach(btn => {
  btn.addEventListener('click', handleDeleteClick)
});

// 表单提交处理
document.getElementById('activityForm').addEventListener('submit', function(e) {
  e.preventDefault();
  const formData = new FormData(this);
  let url = '';
  if(activityAction === 'create') {
    url = '/activity/create/';
    formData.append('dossier_id', currentDossierId);
  } else {
    url = `/activity/${currentActivityId}/update/`;
  }
  formData.append('csrfmiddlewaretoken', csrfToken);

  fetch(url, {
    method: 'POST',
    body: formData,
    headers: {'X-Requested-With': 'XMLHttpRequest'}
  })
  .then(res => res.json())
  .then(data => {
    if(data.status === 'success') {
      activityModal.hide();
      if(activityAction === 'create') {
        // 插入新记录到列表
        const newActivityHtml = `
        <div class="card mb-2 activity-item" data-activity-id="${data.id}">
          <div class="card-body d-flex justify-content-between align-items-center">
            <div>
              <h5 class="card-title">${data.title}</h5>
              <p class="card-text">${data.content}</p>
            </div>
            <div>
              <button class="btn btn-sm btn-outline-secondary editActivityBtn" data-activity-id="${data.id}">编辑</button>
              <button class="btn btn-sm btn-outline-danger deleteActivityBtn" data-activity-id="${data.id}">删除</button>
            </div>
          </div>
        </div>
        `;
        document.getElementById('activityList').insertAdjacentHTML('beforeend', newActivityHtml);
        // 给新元素绑定事件
        const newItem = document.querySelector(`.activity-item[data-activity-id="${data.id}"]`);
        newItem.querySelector('.editActivityBtn').addEventListener('click', handleEditClick);
        newItem.querySelector('.deleteActivityBtn').addEventListener('click', handleDeleteClick);
      } else {
        // 更新现有记录内容
        const item = document.querySelector(`.activity-item[data-activity-id="${currentActivityId}"]`);
        item.querySelector('.card-title').textContent = data.title;
        item.querySelector('.card-text').textContent = data.content;
      }
    } else {
      // 渲染表单错误
      let errorHtml = '';
      for(const field in data.errors) {
        errorHtml += `<p>${data.errors[field].join(',')}</p>`;
      }
      document.getElementById('formErrors').innerHTML = errorHtml;
    }
  })
});

// 编辑按钮公共处理函数
function handleEditClick() {
  activityAction = 'edit';
  currentActivityId = this.dataset.activityId;
  document.getElementById('activityModalLabel').textContent = '编辑活动';
  document.getElementById('formErrors').innerHTML = '';
  // 拉取活动详情填充表单
  fetch(`/activity/${currentActivityId}/detail/`, {
    headers: {'X-Requested-With': 'XMLHttpRequest'}
  })
  .then(res => res.json())
  .then(data => {
    if(data.status === 'success') {
      document.getElementById('id_title').value = data.title;
      document.getElementById('id_content').value = data.content;
      activityModal.show();
    }
  })
}

// 删除按钮公共处理函数
function handleDeleteClick() {
  const activityId = this.dataset.activityId;
  if(confirm('确定要删除这条活动记录吗?')) {
    fetch(`/activity/${activityId}/delete/`, {
      method: 'POST',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'X-CSRFToken': csrfToken
      }
    })
    .then(res => res.json())
    .then(data => {
      if(data.status === 'success') {
        document.querySelector(`.activity-item[data-activity-id="${activityId}"]`).remove();
      }
    })
  }
}

注意:如果使用Bootstrap5以下版本,Modal初始化语法需要对应调整

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 19:27:03