Django ForeignKey无法选择Cast 实现搜索选择关联项方案
实现方案
首先先修正你现有代码的基础bug:Cast_movie模型没有定义name字段,当前写的__str__方法返回self.name会直接抛出属性错误,先把模型调整正确:
class Cast_movie(models.Model): movie = models.ForeignKey('movies.movie', on_delete = models.CASCADE, blank=True, null=True) cast = models.ForeignKey('casts.cast', on_delete = models.CASCADE, blank=True, null=True) class Meta: unique_together = ('movie', 'cast') # 加唯一约束,避免同一部电影重复关联同一个演职人员 def __str__(self): return f"{self.movie.title} - {self.cast.name}"
根据你使用的场景(Django自带后台/自定义前端页面),选对应方案实现即可。
方案1:Django Admin后台实现
如果是要在Django自带的后台电影编辑页实现搜索选演职人员、不加载全量列表的效果:
- 先给演职人员模型的Admin类配置搜索字段
# casts/admin.py from django.contrib import admin from .models import Cast @admin.register(Cast) class CastAdmin(admin.ModelAdmin): search_fields = ['name'] # 开启按姓名字段搜索的支持
- 在电影Admin中用内联形式配置中间表,把演职人员选择控件换成自动补全搜索框
# movies/admin.py from django.contrib import admin from .models import Movie, Cast_movie class CastMovieInline(admin.TabularInline): model = Cast_movie autocomplete_fields = ['cast'] # 该字段会变成搜索框,输入关键词才加载匹配结果,不会拉取全量演职人员 extra = 1 @admin.register(Movie) class MovieAdmin(admin.ModelAdmin): inlines = [CastMovieInline]
配置完成后进入电影编辑页,演职人员选择区域就是搜索输入框,输入姓名关键词会自动展示匹配结果,直接点选即可完成关联。
方案2:自定义前端页面实现
如果是要在自己开发的用户侧电影页面实现该功能,按以下步骤开发:
第一步:编写后端接口
需要三个基础接口:演职人员搜索接口、添加关联接口、移除关联接口。
先写搜索接口,接收关键词返回匹配的演职人员,单次最多返回20条,避免数据量过大:
# movies/views.py from django.http import JsonResponse from casts.models import Cast from .models import Cast_movie def search_cast(request): keyword = request.GET.get('keyword', '').strip() casts = Cast.objects.filter(name__icontains=keyword)[:20].values('id', 'name') return JsonResponse(list(casts), safe=False) def add_cast_relation(request): if request.method != 'POST': return JsonResponse({'code':405, 'msg':'请求方式错误'}) movie_id = request.POST.get('movie_id') cast_id = request.POST.get('cast_id') # 重复关联直接返回成功 if Cast_movie.objects.filter(movie_id=movie_id, cast_id=cast_id).exists(): return JsonResponse({'code':0, 'msg':'ok'}) try: Cast_movie.objects.create(movie_id=movie_id, cast_id=cast_id) return JsonResponse({'code':0, 'msg':'ok'}) except Exception as e: return JsonResponse({'code':500, 'msg':'关联失败'}) def remove_cast_relation(request): if request.method != 'POST': return JsonResponse({'code':405, 'msg':'请求方式错误'}) movie_id = request.POST.get('movie_id') cast_id = request.POST.get('cast_id') Cast_movie.objects.filter(movie_id=movie_id, cast_id=cast_id).delete() return JsonResponse({'code':0, 'msg':'ok'})
把接口注册到路由:
# movies/urls.py from django.urls import path from . import views urlpatterns = [ # 你原有路由保持不变 path('search-cast/', views.search_cast, name='search_cast'), path('add-cast-relation/', views.add_cast_relation, name='add_cast_relation'), path('remove-cast-relation/', views.remove_cast_relation, name='remove_cast_relation'), ]
注意:上线前记得给添加/移除关联的接口加权限校验,只允许有编辑权限的用户调用,避免数据被随意篡改。
第二步:编写页面UI
在电影页面对应位置加搜索框、搜索结果容器、已关联演职人员列表:
<!-- 电影页面模板对应位置 --> <div class="cast-select-wrapper"> <h4>关联演职人员</h4> <!-- 记得在页面全局位置渲染csrftoken隐藏域,供后续POST请求使用 --> {% csrf_token %} <input type="text" id="cast-search" placeholder="输入演职人员姓名搜索"> <div id="search-result" style="display:none; border:1px solid #eee; max-height:220px; overflow-y:auto; margin:8px 0;"></div> <div id="related-casts"> <!-- 渲染当前电影已经关联的演职人员 --> {% for rel in movie.cast_movie_set.all %} <div class="cast-item" data-cast-id="{{rel.cast.id}}"> {{rel.cast.name}} <button class="remove-btn">移除</button> </div> {% endfor %} </div> </div>
第三步:编写前端交互逻辑
加JS实现搜索防抖、结果展示、关联/移除操作的逻辑:
document.addEventListener('DOMContentLoaded', () => { const searchInput = document.getElementById('cast-search') const resultBox = document.getElementById('search-result') const relatedBox = document.getElementById('related-casts') const movieId = '{{ movie.id }}' // 从模板获取当前电影ID const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value let searchTimer = null // 输入关键词触发搜索,加300ms防抖减少无效请求 searchInput.addEventListener('input', () => { clearTimeout(searchTimer) const keyword = searchInput.value.trim() if (!keyword) { resultBox.style.display = 'none' return } searchTimer = setTimeout(() => { fetch(`/search-cast/?keyword=${encodeURIComponent(keyword)}`) .then(res => res.json()) .then(list => { resultBox.innerHTML = '' if (list.length === 0) { resultBox.innerHTML = '<div style="padding:8px;">无匹配结果</div>' } else { list.forEach(cast => { const item = document.createElement('div') item.style.padding = '8px' item.style.cursor = 'pointer' item.textContent = cast.name item.dataset.castId = cast.id // 点击结果项触发关联 item.addEventListener('click', () => { bindCast(cast.id, cast.name) resultBox.style.display = 'none' searchInput.value = '' }) resultBox.appendChild(item) }) } resultBox.style.display = 'block' }) }, 300) }) // 关联演职人员 function bindCast(castId, castName) { if (relatedBox.querySelector(`[data-cast-id="${castId}"]`)) { alert('该演职人员已关联') return } fetch('/add-cast-relation/', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': csrftoken}, body: `movie_id=${movieId}&cast_id=${castId}` }).then(res => res.json()).then(res => { if (res.code !== 0) return alert(res.msg) const node = document.createElement('div') node.className = 'cast-item' node.dataset.castId = castId node.innerHTML = `${castName} <button class="remove-btn">移除</button>` relatedBox.appendChild(node) }) } // 点击页面其他区域关闭搜索结果 document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !resultBox.contains(e.target)) { resultBox.style.display = 'none' } }) // 事件委托处理移除关联操作 relatedBox.addEventListener('click', (e) => { if (e.target.className === 'remove-btn') { const item = e.target.closest('.cast-item') const castId = item.dataset.castId fetch('/remove-cast-relation/', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': csrftoken}, body: `movie_id=${movieId}&cast_id=${castId}` }).then(res => res.json()).then(res => { if (res.code === 0) item.remove() }) } }) })
写完之后页面就可以实现输入关键词搜索演职人员、点选关联、移除关联的功能,全程不会加载全量演职人员列表,性能更好。
内容的提问来源于stack exchange,提问作者user892134
相关产品推荐
相关产品推荐

