Write the Code. Change the World.

分类目录
5月 01

虽然有 docker , 但还是喜欢用 homestead 开发环境。这个时候,一些旧的项目可能会用到比较低版本的 php,这个时候切换 php 版本就是一个常用的操作。

使用 php 版本

homestead 环境中,项目使用 php 的哪个版本,可以在 Homestead.yaml 文件中进行配置。比如:

sites:
    - map: work.xiangrong.com
      to: /home/vagrant/code/zhoulin/work.xiangrong.com/public
      php: "7.4"

这里指定好 php 版本就好。

但是,当 homestead 环境中当前使用的 php 版本是 php 8php 5.6 这些。使用 composer 或项目本身就有可能报错。这个时候,我们就需要将 homestead 环境的 php 版本切换的和项目所配置的一样。

补充

如果切换成对应的 php 版本还会出 badgateway 这样的错误,可以试着重启 php 看看。

# 这里以 php 7.4 为例
sudo /etc/init.d/php7.4-fpm restart

操作

# 查看当前环境中的 php 版本
php -v

# 查看所有 php 版本和当前版本
sudo update-alternatives --display php

# 执行后,会列出当前 php 所有版本和编号,输入编号,切换到执行的版本
sudo update-alternatives --config php

其实

其实,想切换到某个 php 版本。直接输入就可以了,也不用向上边那样敲那么长的命令。 比如,想切换到 php7.4 版本。直接输入 php74 回车即可。同理,想切换到 php8.2 版本,直接输入 php82 回车即可。这些命令,可以在 Homestead 目录下的 aliases 文件中找到。

function php73() {
    sudo update-alternatives --set php /usr/bin/php7.3
    sudo update-alternatives --set php-config /usr/bin/php-config7.3
    sudo update-alternatives --set phpize /usr/bin/phpize7.3
}

function php74() {
    sudo update-alternatives --set php /usr/bin/php7.4
    sudo update-alternatives --set php-config /usr/bin/php-config7.4
    sudo update-alternatives --set phpize /usr/bin/phpize7.4
}

function php80() {
    sudo update-alternatives --set php /usr/bin/php8.0
    sudo update-alternatives --set php-config /usr/bin/php-config8.0
    sudo update-alternatives --set phpize /usr/bin/phpize8.0
}

function php81() {
    sudo update-alternatives --set php /usr/bin/php8.1
    sudo update-alternatives --set php-config /usr/bin/php-config8.1
    sudo update-alternatives --set phpize /usr/bin/phpize8.1
}

function php82() {
    sudo update-alternatives --set php /usr/bin/php8.2
    sudo update-alternatives --set php-config /usr/bin/php-config8.2
    sudo update-alternatives --set phpize /usr/bin/phpize8.2
}

今天五月一日。

3月 14

使用 vite 来作为构建前端的工具,是有一些缺陷的。

However, it lacks some features that Laravel Mix supports, such as the ability to copy arbitrary assets into the build that are not referenced directly in your JavaScript application.

https://laravel.com/docs/10.x/vite

相关

https://github.com/laravel/vite-plugin/blob/main/UPGRADE.md#migrating-from-vite-to-laravel-mix

多语言相关

注册,登录这些页面,以及 request 错误的提示,都是英文的。想要中文的,就得有对应的语言配置。

https://laravel-lang.com/installation/

composer require laravel-lang/common --dev

php artisan lang:add zh_CN

php artisan lang:update

然后,在 config/app.php 中,将 lang 设置成 'zh_CN' 即可。

3月 12

Inertia 是为希望构建整体应用程序的人而构建的。他们通常更喜欢控制器和视图之间的紧密结合,但又希望使用现代客户端框架来构建其应用程序。

上边这个描述,的确是很贴切。比如 laravel 框架,自身使用的是 blade 模板来渲染页面。所有处理都是服务端完成。也可以在其中部分使用 vue、react这些。

Laravel 提供了两全其美的解决方案。Inertia 可以桥接您的 Laravel 应用程序和现代 Vue 或 React 前端,使您可以使用 Vue 或 React 构建完整的现代前端,同时利用 Laravel 路由和控制器进行路由、数据注入和身份验证 - 所有这些都在单个代码存储库中完成。使用这种方法,您可以同时享受 Laravel 和 Vue / React 的全部功能,而不会破坏任何一种工具的能力。

https://laravel.com/docs/10.x/frontend

而 Inertia.js ,则是可以作为一个胶水的目的,就是服务端还是那个服务端,使用 vue,react,svelte 还是一样使用。但这些都是服务端来完成的。传统的 vue, react,svelte 这些(非 ssr 实现的),都是在前端处理,比如状态,页面 dom等都是在前端实现的。

官网: https://inertiajs.com/who-is-it-for

几年前构建的 demo: http://demo.inertiajs.com/login

中文文档(这个文档太旧了,当前最新版本是 1.0 版本):https://learnku.com/docs/inertia/0.11

相关

https://learnku.com/laravel/t/41051

3月 11

文本形式的一问一答,公众号这种场景还是不错。今天就尝试用 laravel10 接入一个 chatgpt 的功能。并使用到公众号上。

使用 chatgpt,我们需要使用下边这个包。

https://packagist.org/packages/geekr/openai-laravel

开始

composer create-project laravel/laravel gzh.com --prefer-dist

# 查看当前 laravel 版本 (Laravel Framework 10.3.3)
php artisan --version

# 添加到版本控制
git init -b main
git add .
git commit -m 'laravel initialize'

# 安装 geekr/openai-laravel
composer require geekr/openai-laravel

如果报 Could not find a version of package geekr/openai-laravel matching your minimum-stability (stable). Require it with an explicit version constraint allowing its desired stability. 这个错误。 可以这么操作。这个包对框架版本限定的死死的,通过这个方式来安装。

rm -rf composer.lock

# 在 composer.json 中增加配置
"require": {
        …
        "geekr/openai-laravel": "dev-master"
    },

composer install

# 添加到版本控制
git add .
git commit -m '安装 geekr/openai-laravel'

如果上边步骤都 ok 了,继续。

# 生成配置文件
php artisan vendor:publish --provider="GeekrOpenAI\Laravel\ServiceProvider"

上边命令,会在 config 目录下,生成一个 openai.php 的配置文件。

然后要配置 OPENAI_API_KEY 。这个 key, 我们在 https://platform.openai.com/account/api-keys 这里申请。

到写这里,已经过去了 2 个多小时。饭也没吃,好难啊。不仅要绿色上网,还要有一些特定国家的手机号码接收短信。接受短信后,询问你用这个来干啥,然后就一直 502,死活过不去。后来用 postman,将 token 以及 body 复制过去请求还是不行。再后来快要放弃的时候,尝试着将 postman body 里边的中文改成字母,竟奇迹般的创建成功了。再然后进入首页,进入 keys 列表。点击创建,马上就可以生成 key 了。


话说为了接受短信,还花了 7 块人民币。

关于接受短信

https://www.hztdst.com/9514.html

https://sms-activate.org/getNumber

上边这种方式是花钱的,花钱的果然好使,好用。后来找啊找,好像也有免费这种服务的。
https://sms24.info/en/messages/OpenAI

https://onlinesim.io/

既然拿到了 key。那就继续。我们在 .env.example 中创建 keyurl 的配置。这样做是因为 .env.example 参与版本控制,.env 是不参与的。

# chatgpt 配置
OPENAI_API_KEY=
OPENAI_BASE_URI=open.aiproxy.xyz/v1

然后,我们在 .env 中配置好上边的配置。填入 keyurl

先用 tinker 进行调试。有问有答就好。

# 打开 tinker
php artisan tinker

# 敲入下边的命令
use GeekrOpenAI\Laravel\Facades\OpenAI;

$content = '中国有多少个王朝,最古老的王朝是哪个王朝';

$messages[] = ['role' => 'user', 'content' => $content];

$response = OpenAI::chat()->create([
    'model' => 'gpt-3.5-turbo',
    'messages' => $messages
]);

坐等结果。看到是有结果返回的。如下图所示。

这仅仅是一个测试,需要多多测试。不过总得持有怀疑的态度。这个接口接的就是 chatgpt,不是其他的接口。给个错误的 key 试试,看是否能有正常的反馈。

只要有问有答,接入公众号就简单的。用户给公众号输入文本信息(如果所有文本信息都用 chatgpt来处理),那就在服务端将接到的信息作为 GeekrOpenAI\Laravel\Facades\OpenAI 的请求参数来直接请求,再将请求结果解析,再处理,拿到结果。然后按照公众号的规则,输出给用户就可以了。

如果你的文本信息还有其他用途。那就得让用户带上标志了。比如让用户在问之前加上前缀 gpt xxxx 这样的。服务端拿到后,再做特殊处理。

话是这么说,可是真正做起来又会出现意外。

gpt 的请求时间太长了。复杂的问题会超出 10 秒,简单的问题也比较久。这个就会引发微信公众号里的异常机制。

腾讯个坑爹货,用户给服务端发送消息,只要 5 秒内,没回复,腾讯会再继续调用,一直调用三次。三次后,还没返回或有错误解构都会显示 “该公众号提供的服务出现故障,请稍后再试”。 网上也有很多解决方法,说直接返回一个空字符串或”success“字符串或使用客服消息。都试过,没有用。空字符串和”success“是没有用,客服消息是有用,只是这玩意不能阻止它连续请求三次的问题。

一直在挣扎。在入口那,用 redis 对请求做一次记录,下次重复请求过滤掉。只是这样做,太不好看了。

还是使用 Redis::setex($key, 20, $key) 来标记。哎。

体验效果如下图。

参考

https://geekr.dev/posts/chatgpt-website-by-laravel-10

3月 09

有这样一个场景。mysql 数据库有个表的字段里边存了一些图片链接信息。关键是这些链接信息是整个域名都包含在内的。如果有一天,域名换了,随着新域名一起,文件也搬过来了。这个时候,就需要把这个字段里边的域名信息全部替换掉。这个时候,REGEXP_REPLACE 就很有用了。

先看看截图和 sql,就知道这个的好用了。

https://blog.vini123.com/wp-content/uploads/2023/03/1678375647317.jpg

UPDATE wp_posts set `post_content` = REGEXP_REPLACE(`post_content`, 'blog.vi', 'blog1.vi') WHERE true;

我这里数据少,之前全部给替换了。

REGEXP_REPLACE

regexp_replace(source, pattern, replace_string, occurrence)

参数说明:

  • source: string类型,要替换的原始字符串
  • pattern: string类型常量,要匹配的正则模式,pattern为空串时抛异常
  • replace_string:string,将匹配的pattern替换成的字符串
  • occurrence: bigint类型常量,必须大于等于0。大于0,表示将第几次匹配替换成replace_string,等于0表示替换掉所有的匹配子串,其它类型或小于0抛异常

返回值:
将source字符串中匹配pattern的子串替换成指定字符串后返回。当输入source, pattern, occurrence参数为NULL时返回NULL,若replace_string为NULL且pattern有匹配,返回NULL,replace_string为NULL但pattern不匹配,则返回原串。

3月 07

服务器安装好 mysql 后。需要先初始化。初始化后,需要创建用户以及分配权限。默认 root 用户是禁用远程登录的。这个时候创建一个远程登录用户就比较重要。对远程登录用户看需要,可以在多个纬度来进行控制。比如限定 ip,就是在指定的 ip 下,才可以访问。然后对权限进行控制,对表进行控制等等。

历史记录: https://blog.vini123.com/382

开始

先创建一个远程可登录用户看看

# 内网登录 mysql
mysql -u root -p

# 创建一个远程可登录用户
create user shenqi@'%' identified by '33441314';

创建用户后,该用户只能连接到数据库服务器,但并没有操作该数据库服务器的权限。

mysql8 以后,必须先创建用户,才可以授权。这里创建用户最好限定ip,这样远程连接必须在该ip下进行。提高数据库的安全性。大多时候,使用 localhost 连接更好。

相关文章

https://www.cnblogs.com/cqdxwjd/p/9925051.html

3月 04

后台的接口授权问题已经解决。现在做移动端的接口。移动端接口路由写在 routes/api.php 中。测试用 postman 就可以。不用去做 ui 界面。

定义一个登录接口和获取用户信息接口

移动端一般都是使用微信授权登录或手机号登录,这样比较贴近场景和方便。为了测试,这里使用邮箱和密码登录。

routes/api.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\UserController;

// 登录
Route::post('login', [UserController::class, 'login'])->name('api.login');

Route::group([
    'middleware' => ['auth:sanctum'],
], function () {
    Route::get('userinfo', [UserController::class, 'getUserInfo'])->name('api.getUserInfo');
});

当前,UserController.php 文件还没有,我们创建一个。

php artisan make:controller Api/UserController

里边加入相应的逻辑。

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Http\Requests\Admin\LoginRequest;
use App\Models\User;

class UserController extends Controller
{
    public function login(LoginRequest $request)
    {
        $data = $request->only(['email', 'password']);

        if (!Auth::validate($data)) {
            return response()->json(['message' => '账号密码错误'], 403);
        }

        $user = User::where('email', $data['email'])->first();

        $user['token'] = $user->createToken('auth')->plainTextToken;

        return response()->json($user);
    }

    public function getUserInfo(Request $request)
    {
        $user = $request->user();

        return response()->json($user);
    }
}

这里依然使用了 LoginRequest 来进行字段的验证。

下边,打开 postman 来进行验证验证。

故意写错密码。返回了错误提示,http 状态码是指定的 403。故意不写 401,401 有它独有的作用。然后修改为正确密码再测试。


这次,返回了正常的数据,也返回了 token。下一步,就通过 token 来获取用户的信息。

这个是在 header 中没有加入 token,返回的结果。这里存在两个问题,第一不能返回正确的结果,第二这里返回的是 html。我们接口通常需要返回的是 json 。给个 html 算啥。这个有两个解决方法,第一就是在发起请求的时候,在 header 中指定 accept 为 application/json 就好。这样框架会知道接口想要什么样的数据。测试如下。

还有一种方式,服务端在数据请求之前主动在header加上 accept。因为很 明确这里是做接口的,那我就干脆直接加上。这个场景,我们新建一个前置中间件就可以完成。

php artisan make:middleware AcceptHeader

# 主动加入 accept

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class AcceptHeader
{
    public function handle(Request $request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

然后,在 $middlewareGroups 的 api 的第一个位置,配置上这个中间件就可以。

App/Http/Kernel.php

        'api' => [
            \App\Http\Middleware\AcceptHeader::class,
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

再来试试,这样就可以了。

最后,我们加上正确的 token。就能正常的获取的用户信息了。

到此。一个简单多端授权使用的功能初步完成了。但对于一个完整的项目,这还远远不够。需要去舔砖加瓦,写更多符合场景需要的业务。

3月 02

vue 这边是可以登录了。可是登录态没有保存。在这里,我们仅仅只要知道用户已经登录或已经注册了就好。然后再通过接口去获取用户信息,然后通过状态管理去处理这些信息。

这里不是通过 token 的方式维护状态。所以登录态仅仅加个标志和生命时间。想象一下,这就是一个独立的后台页面。结合 vue 的生态,它的流程是什么样子的呢。

  1. 判断用户是否登录过了。登录过了就直接请求用户信息。这里的用户信息包括用户自身的信息以及后台菜单的信息。所有权限角色都由后端控制和生成。如果还有其他配置信息,也可以连带或再单独发送请求。 这些请求是异步的,这些请求都在 auth 中间件的守护下。如果 cookie 是过期的,后端会返回 401,前端会重定向到登录页面。如果能正常拿到数据,那么就 next,渲染后台页面(左侧菜单,顶部,中间内容部分)。
  2. 用户没登录过。前端直接重定向到登录页面。

流程图如下:

先完成服务端逻辑

用户信息的获取

增加路由,路由应遵循 restful 的风格。通过请求方式 + 资源名 + 参数的方式。比如,如果是 put 请求,表示该请求是修改已存在的资源。

# routes/admin.php

Route::group([
    'middleware' => ['auth:sanctum'],
], function () {
    Route::get('userinfos', [UserController::class, 'getUserInfo'])->name('admin.api.getUserInfo');
});

增加控制器逻辑

# app/Http/Controllers/Admin/UserController
public function getUserInfo(Request $request)
{
    $user = $request->user();

    return response()->json(['user' => $user, 'menus' => []]);
}

这里可以直接拿到用户信息,是因为有 sanctum 中间件的守护。如果用户的认证未通过,在中间件环节就已经返回 401 了。这里是测试,所以菜单,直接给个空的数组。

json 没有指定 http状态码,默认就是 200。

完成 vue 的逻辑

vue 中的状态管理这里用 pinia。

删除默认的 stores/counter.js

增加 stores/userinfo.js

import { defineStore } from 'pinia'

export const useUserinfo = defineStore('userinfo', {
    state: () =>({
        isLogin: localStorage.getItem('isLogin') || false,
        name: '',
        email: ''
    }),
    actions: {
        login() {
            localStorage.setItem('isLogin', true)
            this.isLogin = true
        },
        loginOut() {
            localStorage.removeItem('isLogin')
            this.isLogin = false
        },
        setUserinfo(value) {
            const keys = ['name', 'email']
            keys.forEach(item => {
                if (value[item]) {
                    this[item] = value[item]
                } else {
                    this[item] = ''
                }
            })
        }
    }
})

现在的 pinia 是以前的 vuex 新版。用起来更方便,如果不是它的功能,感觉就自己组件内的调用。而且,也不像以前那样多很多 modules 的概念。

状态处理好了,我们现在来处理路由守护。将以前的路由里边加上守护逻辑就好。

router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import { useUserinfo } from '@/stores/userinfo.js'
import { GetUserinfo }from '@/api/request'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  base: '/admin/',
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../views/home/index.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/user/login.vue')
    },
    {
      path: '/register',
      name: 'register',
      component: () => import('../views/user/register.vue')
    }
  ]
})

router.beforeEach(async (to, from, next) => {
  const whiteList = ['/login']
  const userinfo = useUserinfo()
  const isLogin = userinfo.isLogin
  if (isLogin) {
    GetUserinfo().then(
      res => {
        userinfo.setUserinfo(res.data.user)
        next()
      }
    ).catch(() => {
      userinfo.loginOut()
      next(`/login?redirect=${to.path}`)
    })
  } else {
    if (whiteList.some(value => value == to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})
export default router

路由守护做好了,我们只需要在登录成功后调用状态的 login 方法,并跳转到首页就好。

关于这个跳转,体验做到更好的话,不是非要强制跳转到首页的。比如本来用户是在看后台的用户信息,这个时候刚好 session 过期了,需要重新登录。登录成功后跳转到之前的页面,就是后台的用户信息页面才是最好的。

关于这个路由,我们可以在登录组件中,通过路由的监听获取到。

最后,我们修改下 header 组件。如果用户登录了,就显示用户的称呼。否则显示登录注册按钮。

<template>
    <header>
        <div>
      <n-avatar round size="large" @click="goHome">
        LOGO
      </n-avatar>
    </div>

        <div class="nav-bar">
            <n-space class="left-nav">
                <n-button quaternary @click="goHome">首页</n-button>
                <n-button quaternary>产品介绍</n-button>
                <n-button quaternary>关于我们</n-button>
            </n-space>

            <n-space v-if="userinfo.name">
                <span>{{ userinfo.name }}</span>
            </n-space>

            <n-space v-else class="right-nav">
                <n-button type="success" size="small" style="font-size: 12px;" @click="goLogin">登录</n-button>
                <n-button type="success" size="small" style="font-size: 12px" @click="goRegister">注册</n-button>
            </n-space>
        </div>
    </header>
</template>

<script setup>

import { useRouter } from 'vue-router'

import { useUserinfo } from '@/stores/userinfo.js'

const userinfo = useUserinfo()

const router = useRouter()

const goHome = () => {
    router.push({name: 'home'})
}

const goLogin = () => {
  router.push({name: 'login'})
}

const goRegister = () => {
  router.push({name: 'register'})
}
</script>

<style lang="scss" scoped>
header {
    display: flex;
    align-items: center;
    box-sizing: border-box;
    padding: 0 30px;
    width: 100%;
    height: 72px;
    background-color: #fff;
    box-shadow: 0 0.125rem 0.25rem #00000013 !important;

  .nav-bar {
    display: flex;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    padding-left: 50px;

    .left-nav {
      flex:1;
    }
  }
}
</style>

退出功能没做。这里就不做了,这里仅仅是 demo。不是真正的项目。

其实,注册按钮也是不需要的。后台我就不想让用户注册。还有登录这里,我其实也不想要用户输入,直接让用户扫码还更快。现在谁没个手机,现在谁没个微信。就是这么霸道,只要一个登录二维码就可以。

这里,我还是把注册相关的删掉吧。删除注册组件,删除注册按钮,删除注册路由信息。

好了。现在打包一下项目。拿到 php 那边去测试一下。

pnpm run build

效果如图所示。

到此,已经验证了。 项目主网站和vue做的spa后台网站,可以公用登录态。下一步就是做移动端(app、小程序等)的接口逻辑。这个接口适用 token 的方式来验证登录态,不再是 cookie和session的方式。

提交代码。清洗车子。收拾房子。

git add .
git commit -m '登录功能彻底完成'

git remote add origin https://gitlab.com/demo9885/vue3_backstage.git

git push -u origin main

代码仓库: https://gitlab.com/demo9885/vue3_backstage

这个只是一个简单的架构。离完整的项目还差很多。比如多语言,多主题(白天,晚上等),组件库,图表,大屏展示等等。

3月 02

服务端 admin/api 的注册登录逻辑处理。

开始

先创建基本的控制器和Request文件。

php artisan make:controller Admin/UserController

php artisan make:request Admin/AdminRequest

php artisan make:request Admin/RegisterRequest

php artisan make:request Admin/LoginRequest

我们先把 request 完成起来。 AdminRequest 作为其他 Request 的父类,需要做两件事。

一是 authorize 永远返回 true。这个是给接口用的,不是网页自己用。没必要去验证通过。验证的逻辑会在路由和中间件中完成。

二是重写 failedValidation 方法。默认的 failedValidation 会发起重定向。这里是接口,也是不需要的。

所以 AdminRequest.php 是这个样子。

<?php

namespace App\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;

class AdminRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json([
            'error' => (new ValidationException($validator))->errors()
        ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY));
    }
}

再看其他 request。

#LoginRequest

<?php

namespace App\Http\Requests\Admin;

class LoginRequest extends AdminRequest
{
    public function rules(): array
    {
        return [
            'email' =>  ['required', 'string', 'email', 'max:255'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function messages()
    {
        return [];
    }

    public function attributes()
    {
        return [
            'email' => '邮箱',
            'password' => '密码'
        ];
    }
}

# RegisterRequest

<?php

namespace App\Http\Requests\Admin;

class RegisterRequest extends AdminRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' =>  ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function messages()
    {
        return [];
    }

    public function attributes()
    {
        return [
            'name' => '称呼',
            'email' => '邮箱',
            'password' => '密码'
        ];
    }
}

在 UserController.php 中创建登录注册接口。

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\LoginRequest;
use App\Http\Requests\Admin\RegisterRequest;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use App\Models\User;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function login(LoginRequest $request)
    {
        $data = $request->only(['email', 'password']);

        $remember = true;

        if (Auth::attempt($data, $remember)) {
            $user = auth()->user();
            return response()->json($user);
        } else {
            return response()->json(['message' => '账号或密码错误'], 403);
        }
    }

    public function register(RegisterRequest $request)
    {
        $data = $request->only(['name', 'email', 'password']);

        $data['password'] = Hash::make($data['password']);

        $user = User::create($data);

        Auth::guard()->login($user);

        return response()->json($user);
    }
}

最后,看看路由。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\UserController;

// 登录
Route::post('login', [UserController::class, 'login'])->name('admin.api.login');

// 注册
Route::post('register', [UserController::class, 'register'])->name('admin.api.register');

csrf-token 路由是 Sanctum 自己提供的。我们只需要再 config/sanctum.php 中配置上前缀就好。

    …
    'prefix' => 'admin/api'

到了这里,服务端的逻辑算是好了。现在用之前创建的 vue 项目来测试登录注册接口。

我们先从简单的入手。那就是登录。登录页面和注册页面很香香。是这样子的。

<template>
    <div class="container">
        <div class="form-wrap">
            <n-card title="登录">
                <n-form
                    ref="formRef"
                    :model="model"
                    :rules="rules"
                    label-placement="left"
                    label-width="auto"
                    require-mark-placement="right-hanging"
                    :style="{
                        maxWidth: '640px'
                    }">
                    <n-form-item label="邮箱" path="email">
                        <n-input v-model:value="model.email" :placeholder="rules.email.message" />
                    </n-form-item>
                    <n-form-item label="密码" path="password">
                        <n-input type="password" v-model:value="model.password" :placeholder="rules.password.message" />
                    </n-form-item>
                    <n-form-item label=" " :show-feedback="false" class="login-item">
                        <n-button strong type="primary" @click="submit">登录</n-button>
                    </n-form-item>
                </n-form>
            </n-card>
        </div>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { GetCsrfCookie, Login } from '@/api/request.js'

const router = useRouter()

const formRef = ref(null)

const model = reactive({
    email: null,
    password: null
})

const rules = {
    email: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入邮箱'
    },
    password: {
        required: true,
        trigger: ['blur', 'input'],
        min: 8,
        message: '请输入密码'
    }
}

const submit = () => {
    formRef.value
        ?.validate((errors) => {
            if (errors) {
                console.error(errors)
            }
        })
        .then(() => {
            doLogin()
        })
}

const doLogin = () => {
    GetCsrfCookie(null).then(() => {
        Login({ email: model.email.trim(), password: model.password.trim() }).then(() => {
            // 登录完成,就跳转到首页吧
            router.push({ name: 'home' })
        })
    })
}
</script>

<style lang="scss" scoped>
.container {
    display: flex;
    justify-content: center;

    .form-wrap {
        display: flex;
        margin: 30px 0;
        min-width: 640px;
    }

    .login-item {
        margin-top: 12px;
    }
}
</style>

好了,我们打个包,试一试。

pnpm run build

然后将打包好的 admin 这个文件夹移动到 laravel 项目的 public 下。

浏览器访问:http://ypb2.com/admin/

ok,显示正常。然后刷新也是没有问题的。再点点登录注册。也可以。好吧,那来登录吧。因为之前通过 laravel 已经注册过一个账号了,就用那个账号登录。

演示如下图:

通过这个可以看到 vue 编写的 spa 页面和 laravel 自己的页面,共同持有 cookie。保证了登录的一致性。

这样做的目的就是想 laravel 的页面是官网。 vue 写的是后台。后台和官网有各自的特点。也有对应的方式去实现(后台用 vue、react 这样实现起来比较好)。

到了这里,还不算完成。虽然 vue 页面登录了。但是对登录的用户信息没有保存处理。下一步,就是对登录的信息进行保存处理。

3月 02

这里来安装 naiveui,将上一步的测试页面搭建完成。

官网: https://www.naiveui.com/zh-CN/os-theme

安装

pnpm add -D naive-ui

流行的组件库,都支持按需引入。我们通过 unplugin-auto-import插件来完成。

pnpm add -D unplugin-vue-components

pnpm add -D unplugin-auto-import

然后再 vite.config.js 中进行配置。

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ['vue']
    }),
    Components({
      resolvers: [
        NaiveUiResolver()
      ]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

通过上边的配置,就可以在项目中,直接使用 naiveui 的组件了。

现在,我们修改下 header.vue。来实现登录注册首页三个页面的跳转。

# header.vue
<template>
    <header>
        <div>
      <n-avatar round size="large" @click="goHome">
        LOGO
      </n-avatar>
    </div>

        <div class="nav-bar">
            <n-space class="left-nav">
                <n-button quaternary @click="goHome">首页</n-button>
                <n-button quaternary>产品介绍</n-button>
                <n-button quaternary>关于我们</n-button>
            </n-space>

            <n-space class="right-nav">
                <n-button type="success" size="small" style="font-size: 12px;" @click="goLogin">登录</n-button>
                <n-button type="success" size="small" style="font-size: 12px" @click="goRegister">注册</n-button>
            </n-space>
        </div>
    </header>
</template>

<script setup>

import { useRouter } from 'vue-router'

const router = useRouter()

const goHome = () => {
    router.push({name: 'home'})
}

const goLogin = () => {
  router.push({name: 'login'})
}

const goRegister = () => {
  router.push({name: 'register'})
}
</script>

<style lang="scss" scoped>
header {
    display: flex;
    align-items: center;
    box-sizing: border-box;
    padding: 0 30px;
    width: 100%;
    height: 72px;
    background-color: #fff;
    box-shadow: 0 0.125rem 0.25rem #00000013 !important;

  .nav-bar {
    display: flex;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    padding-left: 50px;

    .left-nav {
      flex:1;
    }
  }
}
</style>

效果如下:

提交代码。

git add .
git commit -m '安装 Naive以及配置顶部 Ui'

顶部ui以及路由跳转完成了。现在做注册页面的 ui 以及相关逻辑。

注册页面

对注册页面 ui 简单的进行一个布局,对表单数据只进行了基础的验证,就是只要存在就好。更详细的规则没配置。

<template>
    <div class="container">
        <div class="form-wrap">
            <n-card title="注册">
                <n-form
                    ref="formRef"
                    :model="model"
                    :rules="rules"
                    label-placement="left"
                    label-width="auto"
                    require-mark-placement="right-hanging"
                    :style="{
                        maxWidth: '640px'
                    }">
                    <n-form-item label="称呼" path="name">
                        <n-input v-model:value="model.name" :placeholder="rules.name.message" />
                    </n-form-item>
                    <n-form-item label="邮箱" path="email">
                        <n-input v-model:value="model.email" :placeholder="rules.email.message" />
                    </n-form-item>
                    <n-form-item label="密码" path="password">
                        <n-input type="password" v-model:value="model.password" :placeholder="rules.password.message" />
                    </n-form-item>
                    <n-form-item label="确认密码" path="confirmPassword">
                        <n-input type="password" v-model:value="model.confirmPassword" :placeholder="rules.confirmPassword.message" />
                    </n-form-item>

                    <n-form-item label=" " :show-feedback="false" class="register-item">
                        <n-button strong type="primary" @click="submit">注册</n-button>
                    </n-form-item>
                </n-form>
            </n-card>
        </div>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const formRef = ref(null)

const model = reactive({
    name: null,
    email: null,
    password: null,
    confirmPassword: null
})

const rules = {
    name: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入称呼'
    },
    email: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入邮箱'
    },
    password: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入密码'
    },
    confirmPassword: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入确认密码'
    }
}

const submit = () => {
    formRef.value?.validate((errors) => {
        if (errors) {
            console.error(errors)
        }
    }).then(() => {

  })
}
</script>

<style lang="scss" scoped>
.container {
    display: flex;
    justify-content: center;

    .form-wrap {
        display: flex;
        margin: 30px 0;
        min-width: 640px;
    }

    .register-item {
        margin-top: 12px;
    }
}
</style>

效果如下:

提交代码。

git add .
git commit -m '注册页面的搭建'

开始写数据请求部分的逻辑了。一般用 axios、flyio 这类库。这里用 axios。

安装、配置、使用 axios

pnpm add axios

安装好后,然后简单封装一个请求库,src/api/request.js
如下:

import axios from 'axios'

// 全局基本配置
axios.defaults.baseURL = 'http://ypb2.com/admin/api/'
axios.withCredentials = true
axios.timeout = 20000

// 请求拦截器
axios.interceptors.request.use(
    (config) => {
        config.headers = {
            Authorization: `Bearer ${GetToken()}`
        }
        return config
    },
    (error) => {
        return Promise.reject(error)
    }
)

// 响应拦截器
axios.interceptors.response.use(
    (response) => {
        return response
    },
    (error) => {
        return Promise.reject(error)
    }
)

// 获取 token
function GetToken() {
    const token = localStorage.getItem('token')
    const now = new Date().getTime() / 1000
    if (token && token.expires_at > now) {
        return token.token
    }
    return null
}

// scrf-cookie
const GetCsrfCookie = () => {
    return axios.get('csrf-cookie')
}

// 登录
const Login = (data) => {
    return axios.post('login', data)
}

// 注册
const Register = (data) => {
    return axios.post('register', data)
}

export {
    GetCsrfCookie,
    Login,
    Register
}

然后就可以在 register.vue 中用起来了

import { ref, reactive, onMounted } from 'vue'
import { Register } from '@/api/request.js'


…
onMounted(() => {
    Register(null).then()
})

当然,这样肯定是有问题的。一个是跨域,另外一个也是不符合服务端 Sanctum 认证的场景的。默认进来必须先进行一个 csrf-cookie 请求。再进行登录或注册的逻辑。

为了更贴合服务端的请求。我们将 base 设置为 admin,并且将 vue-router 的base也设置成 admin。

# vite.config.js
export default defineConfig({
  base: '/admin',
  build: {
    outDir: 'admin'
  },
  …


# router/index.js
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  base: '/admin/',
  routes: [
  …

于是,访问的地址就变成:http://ypb2.com/admin/

这样在 vite 这里是可以的。但是在 nginx 那边就不行。打开页面再刷新就找不到资源了。对 nginx 也需要配置。这里先配置好。

cd /etc/nginx/sites-enabled

sudo vim ypb2.com

# 添加下边的配置
    location ~* ^\/admin\/((?!api\/).) {
        try_files $uri $uri/ /admin/index.html;
    }

# 重启 nginx
sudo nginx -s reload

这个配置很重要的哈。到此,算是前端打包的配置完成。git 版本控制中将 admin 目录设置为忽略。提交版本控制。

git add .
git commit -m '配置输出文件夹,路由前缀等'