Write the Code. Change the World.

分类目录
7月 06

安装 element-plus

https://element-plus.org/zh-CN/guide/installation.html

pnpm install element-plus

配置按需引入

首先安装 unplugin-vue-componentsunplugin-auto-import 这两款插件。

pnpm add -D unplugin-vue-components unplugin-auto-import

编辑 vite.config.ts,添加以下配置。

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

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

继续阅读

7月 05

只是想用 vue3 + ts + element-plus 技术栈做一个后台管理系统。ui 组件库蛮多的,还是觉得各个方面 element-plus 都不错,就选它。还有一些其他的插件比如 tailwindcss,echarts 等。不想那么咋,就先不用。echarts 后边肯定是会用上。

https://cn.vuejs.org/guide/introduction.html

https://element-plus.org/zh-CN/#/zh-CN

功能点

  1. 多语言(可有可无)
  2. 多主题(这个还是得搞上)
  3. loading
  4. api 请求
  5. 动画
  6. 动态路由(路由全部由接口产生)

开始吧

# 创建项目
pnpm create vue

# 项目名称 simpleAdmin
# 选择使用 ts
# 选择 vue router
# 选择 pinia
# 选择 eslint
# 选择 prettier format code
# 其他我都不要

继续阅读

6月 27

配置、打包、部署

因为想部署到非根目录下。所以需要明确配置该非根目录的目录名。

修改 nuxt.config.js

import postcss from './postcss.config'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  devtools: { enabled: false },
  css: [
    '@/assets/css/main.scss'
  ],
  postcss,
  app: {
    baseURL: '/nuxt/'
  }
})

其中, app: { baseURL: ''} 就是配置目录的。

继续阅读

6月 27

首页就按照原网站的图弄弄。图片资源从以往的库中复制过来。

<template>
    <NuxtLayout name="base">

        <div class="container flex">
            <div class="md:w-1/3 flex flex-col">
                <span class="text-black text-xl font-semibold mt-16">约拍宝</span>
                <a class="text-blue-500 text-sm mt-3" href="https://www.yuepaibao.com" target="_blank">官方网站</a>

                <p class="text-gray-600 text-sm mt-3">约拍宝是一款专业摄影类平台,主要服务于爱拍摄的人群。用户可以在平台上进行交互约定支付等来促成合作,还可以发布作品供其他用户欣赏、评论等。</p>

                <div class="mt-5">
                    <img class="w-40 h-40 shadow-sm rounded-md inline mr-5 mb-4" src="../assets/image/home/ypb_miniapp.jpg" alt="约拍宝小程序"  />
                    <img class="w-40 h-40 shadow-sm rounded-md inline mb-4" src="../assets/image/home/ypb_iosapp.jpg" alt="约拍宝 ios app"  />
                </div>

                <div class="mt-1">
                    <img class="w-40 h-40 shadow-sm rounded-md inline mr-5 mb-4" src="../assets/image/home/ypb_baidu.png" alt="百度小程序"  />
                    <img class="w-40 h-40 shadow-sm rounded-md inline mb-4" src="../assets/image/home/ypb_douyin.png" alt="抖音小程序"  />
                </div>
            </div>

            <div class="md:w-2/3 p-10 flex">
                <img class="w-full object-contain max-w-3xl" src="../assets/image/home/ypb.jpg" />
            </div>
        </div>
    </NuxtLayout>
</template>

效果是这样子的。

继续阅读

6月 26

有追求,自然会把尽力把每一个地方做到最好。有这样一个场景,中间实体内容很少。这个时候不做特殊处理,如果是正常排版下来,底下会出现很大空白。这样就不好看。既然知道了,那就想办法去解决掉。

先截图看看我说的这个场景。底部我还没完善哈。但是底部下边一大片空白。这样体验就不好了。只是一般网页的内容都会比较多,会把底部堆下去。总不能保证每个页面都能堆下去。

继续阅读

6月 25

安装好了 tailwindcss 以及相关的插件后。也安装一下 vscode 对应的插件。常规网站,顶部,底部一般都是公用的,我们可以提取出来,作为 layout。然后是自适应问题,tailwindcss 已居右自适应的特性。我们再扩展一下啊,支持得更宽一点。现在电脑,分辨率也都已经很高了。支持的多一点,体验也会好一些。然后正如 bootstrap 框架一样。container 容器,我们还是要用的。tailwindcss 默认的 container 是不居中的,我们可以在插件中配置一下。默认就居住。其实居中和 bootstrap 一样,都是使用 margin:auto 来搞定的。

那么开始吧。

配置断点和容器

编辑 tailwind.config, 先扩充宽度。也可以更多点。

  …
  theme: {
    extend: {},
    screens: {
      'sm': '640px',
      'md': '768px',
      'lg': '1024px',
      'xl': '1280px',
      '2xl': '1536px',
      '3xl': '1782px',
    }
  },

然后设置容器,让其默认就居中。相同的位置,增加 container 配置。其中,centertrue 时,container 容器默认就居中,这个和 bootstrap 一样了。然后 padding 设置的是每个节点下的内边距。

  …
  theme: {
    extend: {},
    container: {
      center: true,
      padding: {
        DEFAULT: '1rem',
        sm: '2rem',
        lg: '4rem',
        xl: '5rem',
        '2xl': '6rem',
        '3xl': '6rem',
      },
    },
    …

继续阅读

6月 22

还是以 vue 项目为例,去配置和使用 tailwindcss

官网 https://tailwindcss.com/docs/installation/using-postcss

安装 vue 项目

安装 vue 项目

# 创建
pnpm create vue

# 选择配置,尽量选 TypeScript、Router、Pinia、ESLint、Prettier 这些。你做一个大的项目,这些都是必须的

我们把 package.json 中的 type 设置成 module。遵循使用 es 模块规范。

{
  …,
  "type": "module"
}

然后运行起来看看。

继续阅读

5月 05

vue3 出来那么久了。以前的项目 vue2 也该更更更了。新项目那必须得 vue3 了。不过,有些关键的写法,会有些不同。一点点的来。

globalProperties

在 vue2,想定义一个全局的属性(方法或变量),只需要定义在 Vue.prototype 上就可以。然后在 template 中可以直接使用,在 script 中,通过 this 来访问使用。

到了 vue3 这里,就不一样了。这些方法和属性定义在 globalProperties, 就是 app.config.globalProperties 上。

比如:

import App from './App'

import {
    createSSRApp
} from 'vue'

const app = createSSRApp(App)

app.config.globalProperties.$test = (value) => {
    console.log('test ' + value)
}

vue3 是干掉 this 的。使用的时候这样。

import { onMounted, getCurrentInstance } from 'vue'

onMounted(() => {
    const { proxy,  ctx } = getCurrentInstance()
    proxy.$test('全局调用')
})

template 中,则可以直接使用。 getCurrentInstance 还有一个输出 ctx,这个和 vue2 时候的 this 一个样。

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 页面登录了。但是对登录的用户信息没有保存处理。下一步,就是对登录的信息进行保存处理。