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

首次使用AJAX在Laravel中删除项目标签遇404错误求助

Laravel AJAX 删除标签时404错误排查

我正在用Laravel构建作品集项目,需要实现AJAX无刷新删除项目表单中的已有标签功能,这是我第一次使用AJAX。我自认为逻辑正确,但一直收到404错误,应该是请求找不到对应的路由。我核对过删除路由和请求路径,也确认了URL中的ID在数据库中存在,但始终找不到问题所在。

以下是相关代码片段:

视图代码

@section('title', 'Portfolio')

@section('content')

    <div class="admincontainers">
        <div class="mobilemenubar">
            @include('components.dropdown_menu')
        </div>
        <div class="column1">
            @include('components.admin_menu')

        </div>


        <div class="admin_column2">

            @include('components.messages')
            <form action="{{ route('update-project', $project->id) }}" class="projectForm" method="POST"
                enctype="multipart/form-data">
                @csrf
                @method('PUT')
                <div>
                    <label for="name">Projet:</label>
                    <input type="text" name="name" required value="{{ $project->name ?? '' }}">
                </div>
                <div>
                    @if (isset($project->image_1))
                        <img width="100px" src="{{ asset("/storage/images/$project->image_1") }}" alt="">
                    @endif
                    <label for="image_1">Image 1:</label>
                    <input type="file" name="image_1" accept="image/*">
                </div>
                <div>
                    @if (isset($project->image_2))
                        <img width="100px" src="{{ asset("/storage/images/$project->image_2") }}" alt="">
                    @endif
                    <label for="image_2">Image 2:</label>
                    <input type="file" name="image_2" accept="image/*">
                </div>
                <div>
                    @if (isset($project->image_3))
                        <img width="100px" src="{{ asset("/storage/images/$project->image_3") }}" alt="">
                    @endif
                    <label for="image_3">Image 3:</label>
                    <input type="file" name="image_3" accept="image/*">
                </div>
                <div>
                    <label for="description">Description:</label>
                    <textarea name="description" required>{{ $project->description }}</textarea>
                </div>
                <div>
                    <label for="url">URL:</label>
                    <input type="text" name="url" required value="{{ $project->url ?? '' }}">
                </div>
                <div>
                    <label for="customer">Client:</label>
                    <input type="text" name="customer" required value="{{ $project->customer ?? '' }}">
                </div>
                <div class="TagSection">
                    <div>
                        <label for="tags[]">Nouveaux Tags:</label>
                        <select name="tags[]" class="form-control" multiple>
                            @foreach ($tags as $tag)
                                <option value="{{ $tag->id }}">{{ $tag->name }}</option>
                            @endforeach
                        </select>
                    </div>

                    <div>
                        <label for="tags[]">Tags Existants:</label>
                        <select name="existingTags[]" id="existingTags" multiple>
                            @foreach ($project->tags as $tag)
                                <option value="{{ $tag->id }}" selected>{{ $tag->name }}</option>
                            @endforeach
                        </select>

                    </div>
                    <button type="button" id="removeTag">Supprimer</button>
                </div>
                <div>
                    <label for="mission">Mission Réalisée:</label>
                    <select name="mission" class="form-control" multiple>
                        @foreach ($categorie as $category)
                            <option value="{{ $category->id }}">{{ $category->name }}</option>
                        @endforeach
                    </select>

                </div>
                <input type="hidden" name="user_id" value="{{ Auth::id() }}">
                <div>
                    <button type="submit" class="Edit">Editer le projet

                    </button>
                </div>
            </form>
            <script>
                let editButton = document.querySelectorAll('.Edit');

                for (let i = 0; i < editButton.length; i++) {
                    editButton[i].addEventListener('click', (e) => {
                        if (confirm("Souhaitez-vous réellement modifier ce projet ?")) {
                            window.location.href = e.target.firstElementChild.getAttribute('href');
                        } else {
                            e.preventDefault();
                        }
                    });

                }


                let removeTagButton = document.getElementById('removeTag');
                removeTagButton.addEventListener('click', function() {
                    console.log('delete button clicked!');

                    let selectedTags = document.getElementById('existingTags').selectedOptions;
                    let tagIds = [];
                    console.log(tagIds);
                    for (let i = 0; i < selectedTags.length; i++) {
                        tagIds.push(selectedTags[i].value);
                    }
                    let projectId = "{{ $project->id }}";
                    let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
                    let csrfTokenElement = document.querySelector('meta[name="csrf-token"]');
                    let deleteRequest = new XMLHttpRequest();
                    deleteRequest.open('DELETE', '/projects/' + projectId + '/tags', true);
                    deleteRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    deleteRequest.setRequestHeader('X-CSRF-Token', csrfTokenElement.getAttribute('content'));

                    deleteRequest.onreadystatechange = function() {
                        if (deleteRequest.readyState === XMLHttpRequest.DONE) {
                            if (deleteRequest.status === 200) {
                                tagIds.forEach(function(tagId) {
                                    let optionToRemove = document.querySelector('#existingTags option[value="' +
                                        tagId + '"]');
                                    optionToRemove.remove();
                                });
                            } else {
                                console.error(deleteRequest.status);
                            }
                        }
                    };
                    deleteRequest.send('tags=' + JSON.stringify(tagIds));
                });
            </script>
        @endsection

路由代码

Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function () {
    
    Route::delete('/projects/{id}/tags', [ProjectController::class, 'deleteTags'])->name('delete-tags');
}); 

控制器代码

public function deleteTags(Request $request, Projet $id)
{
    dd($request->input('tags'), $id);
    $tags = json_decode($request->input('tags'));
    $id->tags()->detach();
    return response()->json(['message' => 'Tags mis à jour avec succès'], 200);
} 

问题原因及修复方案

  1. 路由前缀缺失:路由定义在admin前缀分组下,但AJAX请求的URL是/projects/{id}/tags,缺少admin前缀,导致路由不匹配触发404。

    • 修复:改用Laravel的route()函数生成正确URL,避免硬编码:
      deleteRequest.open('DELETE', "{{ route('delete-tags', ['id' => $project->id]) }}", true);
      
  2. 路由模型绑定参数优化:控制器方法参数是Projet $id,建议将参数名与模型变量名统一为Projet $project,增强代码可读性,同时可以指定要分离的标签ID,无需全部分离:

    public function deleteTags(Request $request, Projet $project)
    {
        $tags = json_decode($request->input('tags'));
        $project->tags()->detach($tags);
        return response()->json(['message' => 'Tags mis à jour avec succès'], 200);
    }
    
  3. CSRF头标准化:Laravel标准的CSRF请求头是X-CSRF-TOKEN,当前代码中是X-CSRF-Token,虽然不区分大小写,但建议改成标准写法:

    deleteRequest.setRequestHeader('X-CSRF-TOKEN', csrfToken);
    
  4. 请求数据格式优化:如果发送JSON数据,建议设置正确的Content-Type并发送JSON字符串,后端无需额外解码:

    deleteRequest.setRequestHeader('Content-Type', 'application/json');
    deleteRequest.send(JSON.stringify({ tags: tagIds }));
    

    后端对应修改:

    $tags = $request->input('tags');
    $project->tags()->detach($tags);
    

内容的提问来源于stack exchange,提问作者Alexis P.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 06:07:02