前言
前面章节没看过的朋友请先从第一章开始看 第一章。这章主要写登录相关功能。我的构思是这样的。单独账号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>
先回家了 到家了再写