文档
视频地址 https://www.youtube.com/watch?v=iGnlmxA7oM8&list=PL38wFHH4qYZXCW2rlBLNdHi5cv-v_qlXO
视频代码 https://github.com/JonVadar/YouTube_videos/tree/main/Web developer path videos/laravel_Inertia_Vue
CSS file: https://github.com/JonVadar/YouTube_videos/blob/main/tailwind_classes.css
🌐 Lodash docs: https://lodash.com/
🌐 Laravel docs: https://laravel.com/docs
🌐 Vue Js docs: https://vuejs.org/guide/introduction.html
🌐 Inertia Js docs: https://inertiajs.com/
🌐 Vite Js docs: https://vitejs.dev/guide/
🌐 TailwindCSS: https://tailwindcss.com/docs/guides/laravel
安装
laravel 安装 空白项目
安装 vue
npm i vue@latest
安装 Inertia
composer require inertiajs/inertia-laravel
Inertia 的服务端安装使用
https://inertiajs.com/server-side-setup
使用
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
@vite('resources/js/app.js')
@inertiaHead
</head>
<body>
@inertia
</body>
</html>
php artisan inertia:middleware
中间件一旦发布,请在HandleInertiaRequests 中间件到 申请表中的中间件组 文件。 bootstrap/app.php
use App\Http\Middleware\HandleInertiaRequests;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
HandleInertiaRequests::class,
]);
})
客户端安装 文档
https://inertiajs.com/client-side-setup
npm install @inertiajs/vue3
npm i @vitejs/plugin-vue
app.js 文件里追加
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
return pages[`./Pages/${name}.vue`]
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})
vite.config.js 文件
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue(),
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
tailwindcss 安装
https://tailwindcss.com/docs/guides/laravel
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
theme: {
extend: {},
},
plugins: [],
}
app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
路由
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('Home');
});
Route::inertia('/about', 'About', ['user' => '张三']);
视图
About.vue
<script setup>
defineProps({
user: String
})
</script>
<template>
<div>
<h1>about {{ user }} </h1>
</div>
</template>
设置默认布局
https://inertiajs.com/pages#creating-layouts
resources\js\Layouts\Layout.vue
<template>
<div>
<header class=" bg-indigo-500 text-white">
<nav class=" flex items-center justify-between p-4 max-w-screen-lg mx-auto ">
<div class=" space-x-6">
<a href="/">Home</a>
<a href="/about">About</a>
</div>
</nav>
</header>
<main class="max-w-screen-lg mx-auto " >
<!-- 使用插槽 -->
<slot></slot>
</main>
</div>
</template>
resources\js\Pages\Home.vue
<script setup>
import Layout from '../Layouts/Layout.vue'
import demo from '../Layouts/demo.vue'
// 方案二 定义选项
defineOptions({ layout: demo })
</script>
<template>
<!-- 方案1 直接引入使用 -->
<!-- <Layout> <h1>home </h1> </Layout> -->
<h1>home </h1>
</template>
app.js
import './bootstrap';
import '../css/app.css';
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import Layout from './Layouts/Layout.vue'
createInertiaApp({
resolve: name => {
const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
let page = pages[`./Pages/${name}.vue`]
// 设置默认布局 如果页面有layout属性,则使用layout属性的值作为布局 如果没有则自己引入
page.default.layout = page.default.layout || Layout
return page
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})
全局注册组件
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
// 全局注册组件
.component('Head', Head)
.component('Link', Link)
.mount(el)
},
Head and meta
import { Head,Link } from '@inertiajs/vue3'
<!-- 子级的组件可以把上级组件里面对应的内容替换掉 -->
<Head>
<title>Your page title</title>
<meta name="description" content="Your page description">
</Head>
组件回调
app.js文件
createInertiaApp({
title: title => `${title} - My App`,
// ...
})
vue文件
import { Head } from '@inertiajs/vue3'
<Head title="Home">
多个头
meta 标签 设置相同的 head-key 表示他们只能存在一个的
// Layout.vue
import { Head } from '@inertiajs/vue3'
<Head>
<title>My app</title>
<meta head-key="description" name="description" content="This is the default description" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
</Head>
// About.vue
import { Head } from '@inertiajs/vue3'
<Head>
<title>About - My app</title>
<meta head-key="description" name="description" content="This is a page specific description" />
</Head>
Link
代替默认的a标签
import { Link } from '@inertiajs/vue3'
<Link href="/">Home</Link>
<Link href="/logout" method="post" as="button" type="button">Logout</Link>
进度条
一般是等待的地方
app.js
createInertiaApp({
progress: {
// 进度条出现后的延迟,以毫秒为单位…
delay: 250,
// 进度条的颜色...
color: '#29d',
// 是否包含默认的nprogress样式...
includeCSS: true,
// 是否显示nprogress旋转器...
showSpinner: true,
},
// ...
})
公共数据
中间件 HandleInertiaRequests
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
return array_merge(parent::share($request), [
// 同步...
'appName' => config('app.name'),
// 懒散... 如果 $request->user() 返回 null,则不会执行该函数
'auth.user' => fn () => $request->user()
? $request->user()->only('id', 'name', 'email')
: null,
]);
}
}
页面中
<script setup>
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const user = computed(() => page.props.auth.user)
</script>
<template>
<main>
<header>
You are logged in as: {{ user.name }}
<!-- 可通过$page来获取所有的信息 -->
{{ $page.props.auth }}
<!-- 当前组件的名 -->
{{ $page.component }}
</header>
<article>
<slot />
</article>
</main>
</template>
vue路由
地址
https://github.com/tighten/ziggy?tab=readme-ov-file#generating-and-importing-ziggys-configuration
安装
php artisan ziggy:generate
配置
// 引入 这个
import { ZiggyVue } from '../../vendor/tightenco/ziggy'
createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
// 在这里注册注册这个插件
.use(ZiggyVue)
.mount(el)
},
})
使用
在路由文件中 添加name
Route::get('/', function () {
// sleep(2);
return Inertia::render('Home');
})->name('home');
Route::inertia('/about', 'About', ['user' => '张三'])->name('about');
在 app.blade.php 的head里面添加
<head>
<!-- 在这个添加 -->
@routes
</head>
在 vue文件中使用
<Link :href="route('home')" >Home</Link>
<Link :href="route('about')" >About</Link>
滚动
正常点击a标签会跳转页面,会滚动到顶部,
所以需要使用 preserve-scroll 让其保持原有的位置
<Link class="mt-[1400px] block" href="/" preserve-scroll>刷新</Link>
表单
提交表单
<script setup>
import { reactive } from 'vue'
import { router } from '@inertiajs/vue3'
const form = reactive({
first_name: null,
last_name: null,
email: null,
})
function submit() {
router.post('/users', form)
}
</script>
<template>
<form @submit.prevent="submit">
<label for="first_name">First name:</label>
<input id="first_name" v-model="form.first_name" />
<label for="last_name">Last name:</label>
<input id="last_name" v-model="form.last_name" />
<label for="email">Email:</label>
<input id="email" v-model="form.email" />
<button type="submit">Submit</button>
</form>
</template>
服务端校验
class UsersController extends Controller
{
public function index()
{
return Inertia::render('Users/Index', [
'users' => User::all(),
]);
}
public function store(Request $request)
{
User::create($request->validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'email' => ['required', 'max:50', 'email'],
]));
return to_route('users.index');
}
}
错误信息处理
<script setup>
// 使用 useForm
import { useForm } from '@inertiajs/vue3'
const form = useForm({
email: null,
password: null,
remember: false,
})
</script>
<template>
<!-- 提交到对应的路由 -->
<form @submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<!-- 错误信息展示 -->
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<!-- 错误信息展示 -->
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<!-- 禁用按钮 请求等待状态 form.processing -->
<!-- 你可以用processing 用于跟踪当前是否提交了表单的属性。这可能有助于通过禁用提交按钮来防止双重形式的提交。 -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
表单的其他请求方法
form.submit(method, url, options)
form.get(url, options)
form.post(url, options)
form.put(url, options)
form.patch(url, options)
form.delete(url, options)
请求所有的参数
https://inertiajs.com/manual-visits
重置部分参数
form.post(route('register'), {
onError: () => form.reset('password', 'password_confirmation')
})
封装组件
resources\js\Pages\Components\TextInput.vue
<script setup>
const model = defineModel({
type: null,
required: true
})
defineProps({
name: {
type: String,
required: true
},
type: {
type: String,
default: "text"
},
message: String
})
</script>
<template>
<div class="mb-6">
<label for="">{{ name }} </label>
<input :type="type" v-model="model" :class="{ '!ring-red-500': message }">
<small class="error" v-if="message">{{ message }} </small>
</div>
</template>
调用组件
import TextInput from '../Components/TextInput.vue';
<TextInput name="用户名" v-model="form.name" :message="form.errors.name" type="text"></TextInput>
<TextInput name="邮箱" v-model="form.email" :message="form.errors.email" type="email"></TextInput>
用户登录
路由
web.php
Route::inertia('/login', 'Auth/Login')->name('login');
Route::post('/login', [AuthController::class, 'login']);
视图文件 Auth/Login
resources\js\Pages\Auth\Login.vue
<script setup>
import { useForm } from '@inertiajs/vue3'
import TextInput from '../Components/TextInput.vue';
const form = useForm({
email: null,
password: null,
remember: null,
})
const submit = () => {
form.post(route('login'), {
onError: () => form.reset('password', 'remember')
})
}
</script>
<template>
<Head title="登录" />
<h1 class="title">用户登录</h1>
<div class=" w-2/4 mx-auto">
<form @submit.prevent="submit">
<TextInput name="邮箱" v-model="form.email" :message="form.errors.email" type="email"></TextInput>
<TextInput name="密码" v-model="form.password" :message="form.errors.password" type="password"></TextInput>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 justify-center mb-2 ">
<input id="remember" type="checkbox" v-model="form.remember">
<label for="remember">记住登录</label>
</div>
<p class=" text-slate-600 mb-2 ">没有账号
<Link :href="route('register')" class="text-link">去注册</Link>
</p>
</div>
<div>
<button class="primary-btn" :disabled="form.processing">登录</button>
</div>
</form>
</div>
</template>
控制器
// 一般的登录过程
public function login(Request $request)
{
$fields = $request->validate([
'email' => 'required|email',
'password' => 'required'
]);
// 登录成功
if (Auth::attempt($fields, $request->remember)) {
// 重新生成会话
$request->session()->regenerate();
// return redirect()->route('dashboard');
return redirect()->intended('dashboard');
}
// 返回错误 并将错误展示在 只保留 email 错误字段
return back()->withErrors([
'email' => '邮箱或密码错误'
])->onlyInput('email');
}
退出登录 路由中间件
视图
Link 的参数 默认是 a标签 现在这样让他显示为 button 标签 就会隐藏 a标签的链接
可以设置请求方式为post
<Link :href="route('logout')" method="post" as="button" type="button" class="nav-link">退出登录</Link>
路由
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
控制器
// 退出登录 一般退出登录的时候清除操作 三部
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('home');
}
设置点击状态显示
通过判断 当前的组件名 来添加 css 样式
<Link :href="route('login')" class="nav-link"
:class="{ 'bg-slate-500': $page.component == 'Auth/Login' }">登录</Link>
<Link :href="route('register')" class="nav-link"
:class="{ 'bg-slate-500': $page.component == 'Auth/Register' }">注册</Link>
路由中间件
设置不同的权限路由
// 设置经过身份验证的用户才能进入的路由
// 如果没有登录 进入这个权限的路由会进行跳转
// 登录后的用户可以访问的路由
Route::middleware('auth')->group(function () {
Route::inertia('/dashboard', 'Auth/Dashboard')->name('dashboard');
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
});
// 游客可访问的路由
Route::middleware('guest')->group(function () {
Route::inertia('/register', 'Auth/Register')->name('register');
Route::post('/register', [AuthController::class, 'register']);
Route::inertia('/login', 'Auth/Login')->name('login');
Route::post('/login', [AuthController::class, 'login']);
});
上传文件
视图文件
<script setup>
import { useForm } from '@inertiajs/vue3'
import TextInput from '../Components/TextInput.vue';
const form = useForm({
avatar: null,
preview: null
})
// 选择头像
const changeAvatar = (e) => {
// 获取第一个文件
form.avatar = e.target.files[0]
// 将文件的url 赋值给 preview
form.preview = URL.createObjectURL(form.avatar)
}
</script>
<!-- 上传头像 带预览 -->
<div class="grid place-items-center">
<div class="relative w-28 h-28 rounded-full overflow-hidden border border-slate-300">
<label for="avatar" class="absolute inset-0 grid content-end cursor-pointer">
<span class="bg-white/70 pb-2 text-center">头像</span>
</label>
<input type="file" @input="changeAvatar" id="avatar" hidden />
<img class="object-cover w-28 h-28" :src="form.preview ?? 'storage/avatars/default.jpg'" />
</div>
<p class="error mt-2">{{ form.errors.avatar }}</p>
</div>
<!-- 上传头像 不带预览 -->
<div>
<label for="avatar"> 头像 </label>
<input type="file" name="avatar" id="avatar" @input="changeAvatar">
</input>
<small class="error" v-if="form.errors.avatar">{{ form.errors.avatar }} </small>
</div>
控制器
// 校验
$fields = $request->validate([
'avatar' => 'file|nullable|max:1024',
'name' => 'required|max:255',
'email' => ['required', 'max:255', 'email', 'unique:users'],
'password' => ['required', 'min:3', 'confirmed']
]);
// 判断这个字段是否是文件
if ($request->hasFile('avatar')) {
// 如果是的话 把他移动到这个地方 并将原来的字段修改
$fields['avatar'] = Storage::disk('public')->put('avatars', $request->avatar);
}
分页
分页数据展示
路由
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
Route::get('/', function (Request $request) {
return inertia('Home', [
// when 的作用 判断是否有搜索参数 类似于if 参数
'users' => User::when($request->search, function (Builder $query) use ($request) {
$query
->where('name', 'like', '%' . $request->search . '%')
->orWhere('email', 'like', '%' . $request->search . '%')
;
})->paginate(2)->withQueryString(),
// withQueryString 记住搜索参数让这个参数返回到分页的数据
// searchTerm 返回搜索参数
'searchTerm' => $request->search,
'can' => [
// 判断用户是否 存在 在判断用户使用有有这个权限
// 通过策略进行校验是否存在这个权限
'delete_user' =>
Auth::user() ?
Auth::user()->can('delete', User::class) :
null,
]
]);
})->name('home');
视图
<script setup>
import { ref, watch } from 'vue';
import PaginationLinks from './Components/PaginationLinks.vue';
import { router } from '@inertiajs/vue3'
import { throttle } from 'lodash';
const props = defineProps({
users: Object,
searchTerm: String,
can: Object
})
// 通过 searchTerm 返回搜索的数据 让 分页的时候这个数据不清空
// 搜索
const search = ref(props.searchTerm)
// throttle 防抖
// 监听搜索
// preserveState: true 让组件保持原有的惯性 不进行重载
// 默认是重载
watch(search, throttle(
(q) => { router.get('/', { search: q }, { preserveState: true }) }
, 500
))
// 格式化时间
const formatDate = (date) =>
new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
</script>
<template>
<Head :title="`| ${$page.component}`"> </Head>
<!-- <h1>Home 页面</h1> -->
<div class=" flex justify-end mb-4">
<div class=" w-1/4">
<input type="search" placeholder="搜索用户" v-model="search" />
</div>
</div>
<div class="pt-4">
<table>
<thead class=" bg-slate-300">
<tr>
<th>头像</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th v-if="can.delete_user">删除</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users.data">
<td>
<img :src="user.avatar" class="avatar">
</td>
<td>{{ user.name }} </td>
<td>{{ user.email }}</td>
<td>{{ formatDate(user.created_at) }}</td>
<th v-if="can.delete_user"><button class=" bg-red-500 rounded-full w-5 h-5"></button> </th>
</tr>
</tbody>
</table>
<!-- 分页 -->
<PaginationLinks :paginator="users"></PaginationLinks>
<!-- <div>
<Link v-for="link in users.links" :key="link.label" v-html="link.label" :href="link.url" class="p-1 mx-1"
:class="{ 'text-slate-300': !link.url, 'text-blue-500': link.active }">
</Link>
<p class=" text-slate-800 text-sm">
当前第 {{ users.current_page }} 页,共 {{ users.last_page }} 页
</p>
<p class=" text-slate-800 text-sm">
展示第 {{ users.from }} 条,到 {{ users.to }} 条 ,共{{ users.total }} 数据
</p>
</div> -->
</div>
</template>
分页组件
resources\js\Pages\Components\PaginationLinks.vue
<!--eslint-disable vue/no-v-text-v-html-on-component-->
<script setup>
defineProps({
paginator: {
type: Object,
required: true,
},
});
const makeLabel = (label) => {
if (label.includes("上一页")) {
return "<<";
} else if (label.includes("下一页")) {
return ">>";
} else {
return label;
}
};
</script>
<template>
<div class="flex justify-between items-start">
<div class="flex items-center rounded-md overflow-hidden shadow-lg">
<div v-for="link in paginator.links" :key="link.url">
<component
:is="link.url ? 'Link' : 'span'"
:href="link.url"
v-html="makeLabel(link.label)"
class="border-x border-slate-50 w-12 h-12 grid place-items-center bg-white"
:class="{
' hover:bg-slate-300': link.url,
'text-zinc-400': !link.url,
'font-bold text-blue-500': link.active,
}"
/>
</div>
</div>
<p class="text-slate-600 text-sm">
Showing {{ paginator.from }} to {{ paginator.to }} of {{ paginator.total }} results
</p>
</div>
</template>
设置返回数据
with() 控制器中通过with 返回的数据
再通过中间件 share() 方法进行返回
在视图中就通过 $page.props.flash.greet 获取数据
控制器中
// 重定向
return redirect()->route('dashboard')->with('greet','欢迎加入这个应用');
中间件 HandleInertiaRequests
class HandleInertiaRequests extends Middleware
{
public function share(Request $request)
{
return array_merge(parent::share($request), [
'flash' => [
'greet' => fn () => $request->session()->get('greet')
],
]);
}
}
视图
<p v-if="$page.props.flash.greet" class="p-4 bg-green-500 ">{{ $page.props.flash.greet }} </p>
搜索
npm i -g npm
npm i --save lodash
<script setup>
import { router } from '@inertiajs/vue3'
import { throttle } from 'lodash';
const props = defineProps({
users: Object,
searchTerm: String,
can: Object
})
// 通过 searchTerm 返回搜索的数据 让 分页的时候这个数据不清空
// 搜索
const search = ref(props.searchTerm)
// throttle 防抖
// 监听搜索
// preserveState: true 让组件保持原有的惯性
// 默认是重载
watch(search, throttle(
(q) => { router.get('/', { search: q }, { preserveState: true }) }
, 500
))
</script>
<div class=" flex justify-end mb-4">
<div class=" w-1/4">
<input type="search" placeholder="搜索用户" v-model="search" />
</div>
</div>
用户验证
生成策略文件
php artisan make:policy UserPolicy
app\Policies\UserPolicy.php
<?php
namespace App\Policies;
use App\Models\User;
class UserPolicy
{
/**
* 创建一个新的策略实例。
*/
public function delete(User $user)
{
// 判断用户是否是管理员 通过用户邮箱判断 如果是这个邮箱通过策略
return $user->email == '779245779@qq.com';
}
}
使用 通过can() 方法判断 通过校验 为true 不通过为 false
Auth::user()->can('delete', User::class)