使用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
相关产品推荐
相关产品推荐

