博客项目 laravel vue mysql 第二章 登录功能

发布于:2025-07-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

前面章节没看过的朋友请先从第一章开始看 第一章。这章主要写登录相关功能。我的构思是这样的。单独账号admin,不需要注册和权限管理,只实现最简单的登录。

后端

数据库

编辑.env文件

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog-api
DB_USERNAME=root
DB_PASSWORD=root

如果laravel本身自带user表就看下面,如果没有执行生成命令

php artisan make:migration create_users_table

我是自带的,就在原来的上面修改了。
编辑迁移文件database/migrations/2014_10_12_000000_create_users_table.php

// 只修改原来的up方法
public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id(); // 主键,自增ID
            $table->string('name'); // 用户名,用于登录
            $table->string('nickname')->nullable(); // 昵称,用于显示
            $table->string('email')->unique(); // 邮箱,唯一,用于登录或通知
            $table->string('password'); // 密码,使用 bcrypt 加密存储
            $table->string('avatar')->nullable(); // 头像路径
            $table->timestamps(); // 创建时间和更新时间
        });
    }

运行迁移

php artisan migrate

新建种子文件

php artisan make:seeder UserSeeder

编辑database/seeders/UserSeeder.php

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

public function run()
    {
        // 插入管理员账号
        DB::table('users')->insert([
            'name' => 'admin',
            'nickname' => '管理员',
            'email' => 'admin@example.com',
            'password' => Hash::make('123456'), // 使用 bcrypt 加密密码
            'avatar' => 'default-avatar.jpg', // 默认头像路径,根据实际情况设置
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

编辑文件database/seeders/DatabaseSeeder

public function run()
{
    $this->call([
        UserSeeder::class,
    ]);
}

运行

php artisan db:seed

到这一步的时候去查看数据库里user表是否生成数据,有的话继续,没有的话自己检查一下

API实现

创建模型

php artisan make:model User

编辑app/Models/User.php

protected $fillable = ['name', 'nickname', 'email', 'password', 'avatar'];

创建控制器

php artisan make:controller AuthController

编辑控制器app/Http/Controllers/AuthController.php

/**
     * 用户登录并生成认证令牌
     *
     * @param Request $request
     * @return JsonResponse
     * @throws ValidationException
     */
    public function login(Request $request): JsonResponse
    {

        // 验证请求数据
        $credentials = $request->validate([
            'name' => 'required|string',
            'password' => 'required|string',
        ]);

        // 尝试使用用户名查找用户
        $user = User::where('name', $credentials['name'])->first();

        // 验证用户存在且密码正确
        if (!$user || !Hash::check($credentials['password'], $user->password)) {
            return response()->json([
                'message' => '用户名或密码错误',
            ], 401);
        }

        // 删除旧令牌并创建新令牌
        $user->tokens()->delete();
        $token = $user->createToken('blog-token', ['*'])->plainTextToken;

        // 返回响应
        return response()->json([
            'data' => [
                'token' => $token,
                'user' => [
                    'id' => $user->id,
                    'name' => $user->name,
                    'nickname' => $user->nickname,
                    'email' => $user->email,
                    'avatar' => $user->avatar ? asset('storage/' . $user->avatar) : null,
                    'role' => 'admin',
                ],
            ],
            'message' => '登录成功',
        ], 200);
    }

编辑routes/api.php

use App\Http\Controllers\AuthController;

Route::post('/login', [AuthController::class, 'login']);

前端

修改src/views/Home.vue

<template>
    <Header />
    
    <h1>Home</h1>

    <Footer />
</template>

<script setup>
import Header from '@/components/layout/Header.vue'
import Footer from '@/components/layout/Footer.vue'
</script>

<style scoped>
</style>

创建文件src/components/layout/Header.vue

<template>
    <div class="header">
        <div class="container">
            <div class="logo">
                <router-link to="/">Green Beans</router-link>
            </div>
            <div class="nav" :class="{ active: isMenuOpen}">
                <router-link to="/">首页</router-link>
                <router-link target="_blank" to="/login">登录</router-link>
            </div>
            <div class="nav-icon" @click="toggleMenu">
                <span></span>
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref } from 'vue';
const isMenuOpen = ref(false);
const toggleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
</script>

<style scoped lang="scss">
.header {
    background: #fff;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
    position: sticky;
    top: 0;
    z-index: 100;
    .container {
        display: flex;
        justify-content: space-between;
        align-items: center;
        height: 64px;
    }
}
    .logo a {
    font-size: 24px;
    font-weight: 700;
    color: #222;
    }
    .nav {
    display: flex;
    gap: 20px;
    a {
        color: #5c6b77;
        font-size: 16px;
        transition: color 0.3s;
        font-weight: 500;
    }
    }
    .nav a:hover,
    .nav a.router-link-exact-active {
    color: #2563eb;
    }
    .nav-icon {
    display: none;
    cursor: pointer;
    font-size: 24px;
    color: #334155;
    transition: color 0.2s ease;
    }
    .nav-icon:hover {
    color: #2563eb;
    }
    @media (max-width: 768px) {
    .header .container {
        position: relative;
    }
    .nav {
        display: none;
        flex-direction: column;
        position: absolute;
        top: 60px;
        right: 0;
        background-color: #fff;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
        width: 100%;
        padding: 10px;
        border-radius: 0 0 16px 16px;
    }
    .nav-icon {
        display: block;
    }
    .nav.active {
        display: flex;
    }
}
</style>

创建文件src/components/layout/Footer.vue

<template>
    <div class="footer">
        <div class="container">
            <p>
                © {{ currentYear }} Green Beans. All rights reserved.
            </p>
        </div>
    </div>
</template>

<script setup>
import { computed } from 'vue';

const currentYear = computed(() => new Date().getFullYear());
</script>

<style scoped>
.footer {
    background-color: #fff;
    color: #888;
    text-align: center;
    padding: 18px 0 12px 0;
    width: 100%;
    font-size: 13px;
    z-index: 999;
    border-top: 1px solid #f0f1f3;
}
.footer p {
    margin: 0;
    line-height: 1.5;
}

</style>

编辑src/assets/base.scss

.container {
  width:100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
}

修改scr/router/index.js

import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
    },
    {
        path: '/login',
        name: 'Login',
        component: () => import('@/views/Login.vue'),
    },
];

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;

创建src/views/Login.vue

<template>
    <h1>login</h1>
</template>

<script setup>

</script>

<style scoped>

</style>

到这里,你该去点击下顶部的登录,如果能正常跳转到login页,那么之前算是成功,否则就回去检查错误。

OK,继续!

编辑src/views/Login.vue

<template>
    <div class="login-container">
        <div class="login-card">
            <h2 class="widget-title">登录</h2>
            
        </div>
    </div>
</template>

<script setup>



</script>

<style scoped lang="scss">
.login-container {
    background-color: #fafbfc;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    .login-card {
        background-color: #fff;
        border-radius: 16px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
        padding: 32px 28px;
        max-width: 360px;
        width: 100%;
        .widget-title {
            font-size: 24px;
            font-weight: 700;
            color: #222;
            text-align: center;
            margin-bottom: 24px;
        }
    }
}

</style>

先回家了 到家了再写


网站公告

今日签到

点亮在社区的每一天
去签到