laravel-inertia-vue

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Laravel + Inertia.js + Vue 3

Laravel + Inertia.js + Vue 3

Comprehensive patterns for building modern monolithic applications with Laravel, Inertia.js, and Vue 3. Contains 30+ rules for seamless full-stack development.
这是一套使用Laravel、Inertia.js和Vue 3构建现代单体应用的综合模式,包含30+条实现无缝全栈开发的规则。

When to Apply

适用场景

Reference these guidelines when:
  • Creating Inertia page components with Vue 3
  • Handling forms with useForm composable
  • Managing shared data and authentication
  • Implementing persistent layouts
  • Navigating between pages
在以下场景中可参考本指南:
  • 创建基于Vue 3的Inertia页面组件
  • 使用useForm组合式函数处理表单
  • 管理共享数据与认证信息
  • 实现持久化布局
  • 页面间导航

Rule Categories by Priority

按优先级划分的规则类别

PriorityCategoryImpactPrefix
1Page ComponentsCRITICAL
page-
2Forms & ValidationCRITICAL
form-
3Navigation & LinksHIGH
nav-
4Shared DataHIGH
shared-
5LayoutsMEDIUM
layout-
6File UploadsMEDIUM
upload-
7Advanced PatternsLOW
advanced-
优先级类别影响程度前缀
1页面组件关键
page-
2表单与验证关键
form-
3导航与链接
nav-
4共享数据
shared-
5布局
layout-
6文件上传
upload-
7高级模式
advanced-

Quick Reference

快速参考

1. Page Components (CRITICAL)

1. 页面组件(关键)

  • page-props-typing
    - Type page props from Laravel
  • page-component-structure
    - Standard page component pattern
  • page-head-management
    - Title and meta tags with Head
  • page-default-layout
    - Assign layouts to pages
  • page-props-typing
    - 为来自Laravel的页面props添加类型
  • page-component-structure
    - 标准化页面组件模式
  • page-head-management
    - 使用Head管理标题与元标签
  • page-default-layout
    - 为页面分配布局

2. Forms & Validation (CRITICAL)

2. 表单与验证(关键)

  • form-useform-basic
    - Basic useForm usage
  • form-validation-errors
    - Display Laravel validation errors
  • form-processing-state
    - Handle form submission state
  • form-reset-preserve
    - Reset vs preserve form data
  • form-transform
    - Transform data before submit
  • form-useform-basic
    - useForm基础用法
  • form-validation-errors
    - 展示Laravel验证错误信息
  • form-processing-state
    - 处理表单提交状态
  • form-reset-preserve
    - 重置与保留表单数据的处理
  • form-transform
    - 提交前转换数据

3. Navigation & Links (HIGH)

3. 导航与链接(高)

  • nav-link-component
    - Use Link for navigation
  • nav-preserve-state
    - Preserve scroll and state
  • nav-partial-reloads
    - Reload only what changed
  • nav-replace-history
    - Replace vs push history
  • nav-link-component
    - 使用Link组件进行导航
  • nav-preserve-state
    - 保留滚动位置与页面状态
  • nav-partial-reloads
    - 仅重新加载变更内容
  • nav-replace-history
    - 替换与推送历史记录的区别

4. Shared Data (HIGH)

4. 共享数据(高)

  • shared-auth-user
    - Access authenticated user
  • shared-flash-messages
    - Handle flash messages
  • shared-global-props
    - Access global props
  • shared-typescript
    - Type shared data
  • shared-auth-user
    - 访问已认证用户信息
  • shared-flash-messages
    - 处理闪存消息
  • shared-global-props
    - 访问全局props
  • shared-typescript
    - 为共享数据添加类型

5. Layouts (MEDIUM)

5. 布局(中)

  • layout-persistent
    - Persistent layouts pattern
  • layout-nested
    - Nested layouts
  • layout-default
    - Default layout assignment
  • layout-conditional
    - Conditional layouts
  • layout-persistent
    - 持久化布局模式
  • layout-nested
    - 嵌套布局
  • layout-default
    - 默认布局分配
  • layout-conditional
    - 条件式布局

6. File Uploads (MEDIUM)

6. 文件上传(中)

  • upload-basic
    - Basic file upload
  • upload-progress
    - Upload progress tracking
  • upload-multiple
    - Multiple file uploads
  • upload-basic
    - 基础文件上传
  • upload-progress
    - 上传进度追踪
  • upload-multiple
    - 多文件上传

7. Advanced Patterns (LOW)

7. 高级模式(低)

  • advanced-polling
    - Real-time polling
  • advanced-prefetch
    - Prefetch pages
  • advanced-modal-pages
    - Modal as pages
  • advanced-infinite-scroll
    - Infinite scrolling
  • advanced-polling
    - 实时轮询
  • advanced-prefetch
    - 页面预加载
  • advanced-modal-pages
    - 将模态框作为页面处理
  • advanced-infinite-scroll
    - 无限滚动

Essential Patterns

核心模式

Page Component with TypeScript

带TypeScript的页面组件

vue
<!-- resources/js/Pages/Posts/Index.vue -->
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3'

interface Post {
  id: number
  title: string
  excerpt: string
  created_at: string
  author: {
    id: number
    name: string
  }
}

interface Props {
  posts: {
    data: Post[]
    links: { url: string | null; label: string; active: boolean }[]
  }
  filters: {
    search?: string
  }
}

const props = defineProps<Props>()
</script>

<template>
  <Head title="Posts" />

  <div class="container mx-auto py-8">
    <h1 class="text-2xl font-bold mb-6">Posts</h1>

    <div class="space-y-4">
      <article
        v-for="post in props.posts.data"
        :key="post.id"
        class="p-4 bg-white rounded-lg shadow"
      >
        <Link :href="route('posts.show', post.id)">
          <h2 class="text-xl font-semibold hover:text-blue-600">
            {{ post.title }}
          </h2>
        </Link>
        <p class="text-gray-600 mt-2">{{ post.excerpt }}</p>
        <p class="text-sm text-gray-400 mt-2">By {{ post.author.name }}</p>
      </article>
    </div>
  </div>
</template>
vue
<!-- resources/js/Pages/Posts/Index.vue -->
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3'

interface Post {
  id: number
  title: string
  excerpt: string
  created_at: string
  author: {
    id: number
    name: string
  }
}

interface Props {
  posts: {
    data: Post[]
    links: { url: string | null; label: string; active: boolean }[]
  }
  filters: {
    search?: string
  }
}

const props = defineProps<Props>()
</script>

<template>
  <Head title="Posts" />

  <div class="container mx-auto py-8">
    <h1 class="text-2xl font-bold mb-6">Posts</h1>

    <div class="space-y-4">
      <article
        v-for="post in props.posts.data"
        :key="post.id"
        class="p-4 bg-white rounded-lg shadow"
      >
        <Link :href="route('posts.show', post.id)">
          <h2 class="text-xl font-semibold hover:text-blue-600">
            {{ post.title }}
          </h2>
        </Link>
        <p class="text-gray-600 mt-2">{{ post.excerpt }}</p>
        <p class="text-sm text-gray-400 mt-2">By {{ post.author.name }}</p>
      </article>
    </div>
  </div>
</template>

Form with useForm

使用useForm的表单

vue
<!-- resources/js/Pages/Posts/Create.vue -->
<script setup lang="ts">
import { Head, Link, useForm } from '@inertiajs/vue3'

interface Category {
  id: number
  name: string
}

interface Props {
  categories: Category[]
}

const props = defineProps<Props>()

const form = useForm({
  title: '',
  body: '',
  category_id: '',
})

function submit() {
  form.post(route('posts.store'), {
    onSuccess: () => form.reset(),
  })
}
</script>

<template>
  <Head title="Create Post" />

  <form @submit.prevent="submit" class="max-w-2xl mx-auto py-8">
    <div class="mb-4">
      <label for="title" class="block font-medium mb-1">Title</label>
      <input
        id="title"
        v-model="form.title"
        type="text"
        class="w-full border rounded px-3 py-2"
      />
      <p v-if="form.errors.title" class="text-red-500 text-sm mt-1">
        {{ form.errors.title }}
      </p>
    </div>

    <div class="mb-4">
      <label for="category" class="block font-medium mb-1">Category</label>
      <select
        id="category"
        v-model="form.category_id"
        class="w-full border rounded px-3 py-2"
      >
        <option value="">Select a category</option>
        <option v-for="cat in props.categories" :key="cat.id" :value="cat.id">
          {{ cat.name }}
        </option>
      </select>
      <p v-if="form.errors.category_id" class="text-red-500 text-sm mt-1">
        {{ form.errors.category_id }}
      </p>
    </div>

    <div class="mb-4">
      <label for="body" class="block font-medium mb-1">Content</label>
      <textarea
        id="body"
        v-model="form.body"
        rows="10"
        class="w-full border rounded px-3 py-2"
      />
      <p v-if="form.errors.body" class="text-red-500 text-sm mt-1">
        {{ form.errors.body }}
      </p>
    </div>

    <div class="flex gap-4">
      <button
        type="submit"
        :disabled="form.processing"
        class="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {{ form.processing ? 'Creating...' : 'Create Post' }}
      </button>

      <Link :href="route('posts.index')" class="px-4 py-2 border rounded">
        Cancel
      </Link>
    </div>
  </form>
</template>
vue
<!-- resources/js/Pages/Posts/Create.vue -->
<script setup lang="ts">
import { Head, Link, useForm } from '@inertiajs/vue3'

interface Category {
  id: number
  name: string
}

interface Props {
  categories: Category[]
}

const props = defineProps<Props>()

const form = useForm({
  title: '',
  body: '',
  category_id: '',
})

function submit() {
  form.post(route('posts.store'), {
    onSuccess: () => form.reset(),
  })
}
</script>

<template>
  <Head title="Create Post" />

  <form @submit.prevent="submit" class="max-w-2xl mx-auto py-8">
    <div class="mb-4">
      <label for="title" class="block font-medium mb-1">Title</label>
      <input
        id="title"
        v-model="form.title"
        type="text"
        class="w-full border rounded px-3 py-2"
      />
      <p v-if="form.errors.title" class="text-red-500 text-sm mt-1">
        {{ form.errors.title }}
      </p>
    </div>

    <div class="mb-4">
      <label for="category" class="block font-medium mb-1">Category</label>
      <select
        id="category"
        v-model="form.category_id"
        class="w-full border rounded px-3 py-2"
      >
        <option value="">Select a category</option>
        <option v-for="cat in props.categories" :key="cat.id" :value="cat.id">
          {{ cat.name }}
        </option>
      </select>
      <p v-if="form.errors.category_id" class="text-red-500 text-sm mt-1">
        {{ form.errors.category_id }}
      </p>
    </div>

    <div class="mb-4">
      <label for="body" class="block font-medium mb-1">Content</label>
      <textarea
        id="body"
        v-model="form.body"
        rows="10"
        class="w-full border rounded px-3 py-2"
      />
      <p v-if="form.errors.body" class="text-red-500 text-sm mt-1">
        {{ form.errors.body }}
      </p>
    </div>

    <div class="flex gap-4">
      <button
        type="submit"
        :disabled="form.processing"
        class="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {{ form.processing ? 'Creating...' : 'Create Post' }}
      </button>

      <Link :href="route('posts.index')" class="px-4 py-2 border rounded">
        Cancel
      </Link>
    </div>
  </form>
</template>

Persistent Layout

持久化布局

vue
<!-- resources/js/Layouts/AppLayout.vue -->
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()
const auth = computed(() => page.props.auth as { user: { name: string } })
</script>

<template>
  <div class="min-h-screen bg-gray-100">
    <nav class="bg-white shadow">
      <div class="container mx-auto px-4 py-3 flex justify-between">
        <Link href="/" class="font-bold">My App</Link>
        <span>Welcome, {{ auth.user.name }}</span>
      </div>
    </nav>

    <main class="container mx-auto px-4 py-8">
      <slot />
    </main>
  </div>
</template>
vue
<!-- resources/js/Pages/Dashboard.vue -->
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue'

defineOptions({ layout: AppLayout })
</script>

<template>
  <h1>Dashboard</h1>
</template>
vue
<!-- resources/js/Layouts/AppLayout.vue -->
<script setup lang="ts">
import { Link, usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()
const auth = computed(() => page.props.auth as { user: { name: string } })
</script>

<template>
  <div class="min-h-screen bg-gray-100">
    <nav class="bg-white shadow">
      <div class="container mx-auto px-4 py-3 flex justify-between">
        <Link href="/" class="font-bold">My App</Link>
        <span>Welcome, {{ auth.user.name }}</span>
      </div>
    </nav>

    <main class="container mx-auto px-4 py-8">
      <slot />
    </main>
  </div>
</template>
vue
<!-- resources/js/Pages/Dashboard.vue -->
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue'

defineOptions({ layout: AppLayout })
</script>

<template>
  <h1>Dashboard</h1>
</template>

Laravel Controller

Laravel控制器

php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

class PostController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Posts/Index', [
            'posts' => Post::with('author:id,name')
                ->latest()
                ->paginate(10),
            'filters' => request()->only('search'),
        ]);
    }

    public function create(): Response
    {
        return Inertia::render('Posts/Create', [
            'categories' => Category::all(['id', 'name']),
        ]);
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        $post = Post::create([
            ...$request->validated(),
            'user_id' => auth()->id(),
        ]);

        return redirect()
            ->route('posts.show', $post)
            ->with('success', 'Post created successfully.');
    }

    public function show(Post $post): Response
    {
        return Inertia::render('Posts/Show', [
            'post' => $post->load('author', 'category'),
        ]);
    }
}
php
<?php

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

class PostController extends Controller
{
    public function index(): Response
    {
        return Inertia::render('Posts/Index', [
            'posts' => Post::with('author:id,name')
                ->latest()
                ->paginate(10),
            'filters' => request()->only('search'),
        ]);
    }

    public function create(): Response
    {
        return Inertia::render('Posts/Create', [
            'categories' => Category::all(['id', 'name']),
        ]);
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        $post = Post::create([
            ...$request->validated(),
            'user_id' => auth()->id(),
        ]);

        return redirect()
            ->route('posts.show', $post)
            ->with('success', 'Post created successfully.');
    }

    public function show(Post $post): Response
    {
        return Inertia::render('Posts/Show', [
            'post' => $post->load('author', 'category'),
        ]);
    }
}

Shared Data (HandleInertiaRequests)

共享数据(HandleInertiaRequests)

php
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user() ? [
                    'id' => $request->user()->id,
                    'name' => $request->user()->name,
                    'email' => $request->user()->email,
                ] : null,
            ],
            'flash' => [
                'success' => $request->session()->get('success'),
                'error' => $request->session()->get('error'),
            ],
        ]);
    }
}
php
<?php

namespace App\Http\Middleware;

use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user() ? [
                    'id' => $request->user()->id,
                    'name' => $request->user()->name,
                    'email' => $request->user()->email,
                ] : null,
            ],
            'flash' => [
                'success' => $request->session()->get('success'),
                'error' => $request->session()->get('error'),
            ],
        ]);
    }
}

Flash Messages Component

闪存消息组件

vue
<!-- resources/js/Components/FlashMessages.vue -->
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed, ref, watch } from 'vue'

const page = usePage()
const flash = computed(() => page.props.flash as { success?: string; error?: string })
const visible = ref(false)
let timer: ReturnType<typeof setTimeout> | null = null

watch(flash, (newFlash) => {
  if (newFlash.success || newFlash.error) {
    visible.value = true
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      visible.value = false
    }, 3000)
  }
}, { deep: true })
</script>

<template>
  <div v-if="visible" class="fixed top-4 right-4 z-50">
    <div
      v-if="flash.success"
      class="bg-green-500 text-white px-4 py-2 rounded shadow"
    >
      {{ flash.success }}
    </div>
    <div
      v-if="flash.error"
      class="bg-red-500 text-white px-4 py-2 rounded shadow"
    >
      {{ flash.error }}
    </div>
  </div>
</template>
vue
<!-- resources/js/Components/FlashMessages.vue -->
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3'
import { computed, ref, watch } from 'vue'

const page = usePage()
const flash = computed(() => page.props.flash as { success?: string; error?: string })
const visible = ref(false)
let timer: ReturnType<typeof setTimeout> | null = null

watch(flash, (newFlash) => {
  if (newFlash.success || newFlash.error) {
    visible.value = true
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      visible.value = false
    }, 3000)
  }
}, { deep: true })
</script>

<template>
  <div v-if="visible" class="fixed top-4 right-4 z-50">
    <div
      v-if="flash.success"
      class="bg-green-500 text-white px-4 py-2 rounded shadow"
    >
      {{ flash.success }}
    </div>
    <div
      v-if="flash.error"
      class="bg-red-500 text-white px-4 py-2 rounded shadow"
    >
      {{ flash.error }}
    </div>
  </div>
</template>

How to Use

使用方法

Read individual rule files for detailed explanations and code examples:
rules/form-useform-basic.md
rules/page-props-typing.md
rules/layout-persistent.md
阅读单个规则文件获取详细说明和代码示例:
rules/form-useform-basic.md
rules/page-props-typing.md
rules/layout-persistent.md

Project Structure

项目结构

laravel-inertia-vue/
├── SKILL.md                 # This file - overview and examples
├── README.md                # Quick reference guide
├── AGENTS.md                # Integration guide for AI agents
├── metadata.json            # Skill metadata and references
└── rules/
    ├── _sections.md         # Rule categories and priorities
    ├── _template.md         # Template for new rules
    ├── page-*.md            # Page component patterns (6 rules)
    ├── form-*.md            # Form handling patterns (8 rules)
    ├── nav-*.md             # Navigation patterns (5 rules)
    ├── shared-*.md          # Shared data patterns (4 rules)
    └── layout-*.md          # Layout patterns (1 rule)
laravel-inertia-vue/
├── SKILL.md                 # 本文档 - 概览与示例
├── README.md                # 快速参考指南
├── AGENTS.md                # AI集成指南
├── metadata.json            # 技能元数据与参考链接
└── rules/
    ├── _sections.md         # 规则类别与优先级
    ├── _template.md         # 新规则模板
    ├── page-*.md            # 页面组件模式(6条规则)
    ├── form-*.md            # 表单处理模式(8条规则)
    ├── nav-*.md             # 导航模式(5条规则)
    ├── shared-*.md          # 共享数据模式(4条规则)
    └── layout-*.md          # 布局模式(1条规则)

References

参考资料

License

许可证

MIT License. This skill is provided as-is for educational and development purposes.
MIT许可证。本技能仅用于教育和开发目的,按现状提供。

Metadata

元数据

  • Version: 1.0.0
  • Last Updated: 2026-03-17
  • Maintainer: Asyraf Hussin
  • Rule Count: 24 rules across 6 categories
  • Tech Stack: Laravel 12+, Inertia.js 2.0+, Vue 3.3+, TypeScript 5+
  • 版本: 1.0.0
  • 最后更新: 2026-03-17
  • 维护者: Asyraf Hussin
  • 规则数量: 6个类别共24条规则
  • 技术栈: Laravel 12+, Inertia.js 2.0+, Vue 3.3+, TypeScript 5+