st779779

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

文档

视频地址 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 安装 空白项目

alt text

安装 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>

搜索

安装 https://lodash.com/

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)

posted on 2024-08-27 13:49  xirang熙攘  阅读(148)  评论(0编辑  收藏  举报