Vue+Laravel+Inertia更新Job资源时出现404错误求助
Job资源更新时出现404错误(Laravel + Inertia + Vue)
我采用Vue作为前端、Laravel作为后端,通过Inertia实现前后端数据传递。尝试更新Job资源时触发404错误,但执行php artisan route:list显示update路由存在:
PUT|PATCH jobs/{job} ................................... jobs.update › JobController@update
相关代码
web.php
<?php use App\Http\Controllers\JobController; use App\Http\Controllers\ProfileController; use Illuminate\Support\Facades\Route; use Inertia\Inertia; use App\Models\User; use App\Models\Job; use Illuminate\Support\Facades\Auth; /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return redirect('/jobs'); }); Route::get('/admin', function () { return Inertia::render('Admin'); })->middleware(['admin', 'verified'])->name('admin'); Route::get('/dashboard', function (\Illuminate\Http\Request $request) { $limit = $request->get('limit', 25); $offset = $request->get('offset', 0); $query = Job::query() ->where('id', $request->user()->id) ->with('tags', 'clicks') ->latest(); $jobs = $query->skip($offset)->take($limit)->get(); return Inertia::render('Dashboard', [ 'jobs' => $jobs ]); })->middleware(['auth', 'verified'])->name('dashboard'); Route::resource('jobs', JobController::class); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); require __DIR__.'/auth.php'; Route::get('/{job}/apply', [JobController::class, 'apply']) ->name('jobs.apply'); Route::get('tags', [JobController::class, 'tags']) ->name('jobs.tags');
JobController.php
<?php namespace App\Http\Controllers; use App\Models\Job; use App\Models\Tag; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Database\Eloquent\Builder; use Inertia\Inertia; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Response; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Carbon\Carbon; class JobController extends Controller { /** * Display a job listing of the resource. */ public function index(Request $request) { $query = Job::query() ->where('is_active', true) ->whereDate('created_at', '>=', Carbon::now()->subDays(30)) ->with('tags') ->latest(); if ($request->has('tag')) { $tag = $request->get('tag'); $query->whereHas('tags', function (Builder $builder) use ($tag) { $builder->where('slug', $tag); }); } //Search if ($request->has('s')) { $search = strtolower($request->get('s')); $query->where(function($query) use ($search) { $query->where('title', 'like', '%' . $search . '%') ->orWhere('company', 'like', '%' . $search . '%') ->orWhere('location', 'like', '%' . $search . '%'); }); } // Pagination $limit = $request->get('limit', 25); $offset = $request->get('offset', 0); $jobs = $query->skip($offset)->take($limit)->get(); $tags = Tag::whereHas('jobs')->groupBy('id')->orderBy('name')->get(); return Inertia::render('Jobs/Index', [ 'jobs' => $jobs, 'tags' => $tags, ]); } /** * Show the form for creating a new resource. */ public function create() { return Inertia::render('Jobs/Create'); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $validationArray = [ 'title' => 'required', 'company' => 'required', 'logo' => 'file|max:2048', 'location' => 'required', 'apply_link' => 'required|url', 'payment_method_id' => 'required' ]; if (!Auth::check()) { $validationArray = array_merge($validationArray, [ 'email' => 'required|email|unique:users', 'password' => 'required|confirmed|min:5', 'name' => 'required' ]); } $request->validate($validationArray); $user = Auth::user(); if (!$user) { $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password) ]); $user->createAsStripeCustomer(); Auth::login($user); } try { $amount = 29900; if ($request->filled('is_highlighted')) { $amount += 1900; } $user->charge($amount, $request->payment_method_id); $job = $user->jobs() ->create([ 'title' => $request->title, 'slug' => Str::slug($request->title) . '-' . rand(1111, 9999), 'company' => $request->company, 'logo' => basename($request->file('logo')->store('public')), 'location' => $request->location, 'apply_link' => $request->apply_link, 'is_highlighted' => $request->filled('is_highlighted'), 'is_active' => true ]); foreach(explode(',', $request->tags) as $requestTag) { $tag = Tag::firstOrCreate([ 'slug' => Str::slug(trim($requestTag)) ], [ 'name' => ucwords(trim($requestTag)) ]); $tag->jobs()->attach($job->id); } return to_route('dashboard'); } catch(\Exception $e) { return redirect()->back() ->withErrors(['error' => $e->getMessage()]); } } /** * Display the specified resource. */ public function show(Job $job, Request $request) { return Inertia::render('Jobs/Job', [ 'job' => $job, 'tags' => $job->tags, ]); } /** * Show the form for editing the specified resource. */ public function edit(Job $job) { return Inertia::render('Jobs/Edit', [ 'job' => $job, 'tags' => $job->tags, ]); } /** * Update the specified resource in storage. */ public function update(Request $request, Job $job) { $data = $request->validate([ 'title' => 'required', 'company' => 'required', 'location' => 'required', 'apply_link' => 'required|url', ]); if ($request->hasFile('logo')) { $data['logo'] = basename($request->file('logo')->store('public')); } try { $job->update($data); return redirect()->route('dashboard'); } catch(\Exception $e) { return redirect()->back() ->withErrors(['error' => $e->getMessage()]); } } /** * Remove the specified resource from storage. */ public function destroy(Job $job) { // } public function apply(Job $job, Request $request) { $job->clicks() ->create([ 'user_agent' => $request->userAgent(), 'ip' => $request->ip() ]); return redirect()->to($job->apply_link); } public function tags() { $tags = Tag::all(); return Inertia::render('Jobs/Tags', [ 'tags' => $tags, ]); } }
Edit.vue
<template> <app-layout> <div class="container mx-auto px-4 mt-10"> <!-- Form Section --> <div class="bg-gray-100 p-6 rounded-lg shadow-md"> <div class="mb-4 pl-4 mt-4"> <h2 class="text-2xl font-medium text-gray-900 title-font"> Edit Job Listing </h2> </div> <form id="edit_form" class="bg-gray-100 p-4" enctype="multipart/form-data"> <!-- Form Fields --> <div class="mb-4 mx-2"> <label for="title" class="block font-medium text-sm text-gray-700">Job Title</label> <input id="title" class="block mt-1 w-full" type="text" name="title" v-model="form.title" required /> </div> <!-- Company Name --> <div class="mb-4 mx-2"> <label for="company" class="block font-medium text-sm text-gray-700">Company Name</label> <input id="company" class="block mt-1 w-full" type="text" name="company" v-model="form.company" required /> </div> <!-- Company Logo --> <div class="mb-4 mx-2"> <label for="logo" class="block font-medium text-sm text-gray-700">Company Logo</label> <input type="file" id="logo" class="mt-1 w-full" ref="logoInput" accept="image/*" @change="onFileChange" /> <img v-if="form.logo" :src="'/storage/' + form.logo" alt="Company Logo" class="mt-2 h-20 w-20 object-cover" /> </div> <!-- Apply Link --> <div class="mb-4 mx-2"> <label for="apply_link" class="block font-medium text-sm text-gray-700">Link To Apply</label> <input id="apply_link" class="block mt-1 w-full" type="text" name="apply_link" v-model="form.apply_link" required /> </div> <!-- Job Tags --> <div class="mb-4 mx-2"> <label for="tags" class="block font-medium text-sm text-gray-700">Tags (separate by comma)</label> <input type="text" id="tags" class="block mt-1 w-full" v-model="tagsInput" placeholder="Enter tags separated by commas" /> </div> <!-- Job Location --> <div class="mb-4 mx-2"> <label for="location" class="block font-medium text-sm text-gray-700">Location</label> <input id="location" class="block mt-1 w-full" type="text" name="location" v-model="form.location" required /> </div> <!-- Submit Button --> <div class="flex justify-end mt-4"> <button type="submit" @click="submitForm($event)" class="bg-indigo-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> Update Listing </button> </div> </form> </div> </div> </app-layout> </template> <script setup> import AppLayout from '../../Components/AppLayout.vue'; import { usePage, useForm } from '@inertiajs/vue3' import { ref, toRefs, watchEffect } from 'vue'; const page = usePage(); const form = useForm({ title: page.props.job.title, company: page.props.job.company, logo: page.props.job.logo, apply_link: page.props.job.apply_link, tags: page.props.job.tags.map(tag => tag.name), location: page.props.job.location, }); // methods const onFileChange = (e) => { let files = e.target.files || e.dataTransfer.files; if (!files.length) return; form.logo = files[0]; }; const submitForm = (event) => { event.preventDefault(); form.put(route('jobs.update', page.props.job.id), { onError: () => console.log(form.errors) }); }; const tagsInput = ref(form.tags.join(', ')); watchEffect(() => { form.tags = tagsInput.value.split(',').map(tag => tag.trim()); }); </script>
原因分析与修复方案
1. 路由优先级冲突(核心问题)
你在Route::resource('jobs', JobController::class)之后定义了Route::get('/{job}/apply', ...),Laravel路由匹配是按顺序执行的,当请求jobs/1时,框架会把jobs解析为{job}参数,优先匹配到/{job}/apply路由,导致update路由无法被命中,返回404。
修复:调整路由顺序,将资源路由放在自定义路由之后:
// 先定义自定义路由 Route::get('/{job}/apply', [JobController::class, 'apply'])->name('jobs.apply'); Route::get('tags', [JobController::class, 'tags'])->name('jobs.tags'); // 再定义资源路由 Route::resource('jobs', JobController::class);
2. 隐式模型绑定验证
检查Job模型的主键是否为id(默认配置无需修改),同时确认前端传递的page.props.job.id是数据库中存在的有效记录ID,避免因模型绑定失败返回404。
3. 额外优化建议
- 在Edit.vue中添加加载状态,防止重复提交:
<button type="submit" @click="submitForm($event)" :disabled="form.processing" class="bg-indigo-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> {{ form.processing ? '更新中...' : 'Update Listing' }} </button>
- 在JobController的update方法中添加权限验证,确保只有Job所有者能修改:
public function update(Request $request, Job $job) { abort_if($job->user_id !== auth()->id(), 403); // 原有验证与更新逻辑 }
内容的提问来源于stack exchange,提问作者Caleb Oki
相关产品推荐
相关产品推荐

